From f484d5f9eded1f4e72bca6ede585240ee4dfa253 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 10:21:30 +0100 Subject: [PATCH 01/62] feat(scripts): add AI artifacts registry schema and validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add ai-artifacts-registry.json with persona definitions and artifact metadata - add Validate-ArtifactRegistry.ps1 for structure and filesystem validation - add JSON schema for registry format validation - integrate lint:registry into npm lint:all pipeline Refs #432 ๐Ÿ”ง - Generated by Copilot --- .github/ai-artifacts-registry.json | 799 ++++++++++++++++++ package.json | 5 +- scripts/linting/Validate-ArtifactRegistry.ps1 | 643 ++++++++++++++ .../schemas/ai-artifacts-registry.schema.json | 195 +++++ 4 files changed, 1640 insertions(+), 2 deletions(-) create mode 100644 .github/ai-artifacts-registry.json create mode 100644 scripts/linting/Validate-ArtifactRegistry.ps1 create mode 100644 scripts/linting/schemas/ai-artifacts-registry.schema.json diff --git a/.github/ai-artifacts-registry.json b/.github/ai-artifacts-registry.json new file mode 100644 index 00000000..224589a1 --- /dev/null +++ b/.github/ai-artifacts-registry.json @@ -0,0 +1,799 @@ +{ + "$schema": "../scripts/linting/schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "hve-core-all": { + "name": "HVE Core All", + "description": "Full HVE-Core release including all artifacts regardless of role-specific filtering" + }, + "developer": { + "name": "Developer", + "description": "Software engineers writing code" + }, + "tpm": { + "name": "Technical PM", + "description": "Program/product managers" + }, + "devops": { + "name": "DevOps Engineer", + "description": "Platform, SRE, infrastructure engineers" + }, + "architect": { + "name": "Software Architect", + "description": "Solution/system architects" + }, + "technical-writer": { + "name": "Technical Writer", + "description": "Documentation specialists" + } + } + }, + "agents": { + "ado-prd-to-wit": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "ado", + "planning" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [ + "ado-wit-planning" + ], + "skills": [] + } + }, + "adr-creation": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "architecture", + "documentation" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "arch-diagram-builder": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "architecture", + "documentation" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "brd-builder": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "planning", + "documentation" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "doc-ops": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "documentation" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [ + "writing-style", + "markdown", + "commit-message" + ], + "skills": [] + } + }, + "gen-data-spec": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "data-science" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "gen-jupyter-notebook": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "data-science" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "gen-streamlit-dashboard": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "data-science" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "github-issue-manager": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github" + ], + "requires": { + "agents": [], + "prompts": [ + "github-add-issue" + ], + "instructions": [ + "markdown" + ], + "skills": [] + } + }, + "hve-core-installer": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "tooling" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "memory": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "rpi", + "session" + ], + "requires": { + "agents": [ + "rpi-agent" + ], + "prompts": [ + "checkpoint", + "rpi" + ], + "instructions": [], + "skills": [] + } + }, + "pr-review": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github", + "review" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "prd-builder": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "planning", + "documentation" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "prompt-builder": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "prompt-engineering" + ], + "requires": { + "agents": [], + "prompts": [ + "prompt-build", + "prompt-refactor", + "prompt-analyze" + ], + "instructions": [ + "prompt-builder" + ], + "skills": [] + } + }, + "rpi-agent": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "rpi", + "orchestration" + ], + "requires": { + "agents": [ + "task-researcher", + "task-planner", + "task-implementor", + "task-reviewer", + "memory" + ], + "prompts": [ + "task-research", + "task-plan", + "task-implement", + "task-review", + "rpi", + "checkpoint" + ], + "instructions": [], + "skills": [ + "video-to-gif" + ] + } + }, + "security-plan-creator": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "architecture", + "security" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "task-implementor": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "rpi", + "implementation" + ], + "requires": { + "agents": [ + "task-reviewer" + ], + "prompts": [], + "instructions": [ + "commit-message" + ], + "skills": [] + } + }, + "task-planner": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "rpi", + "planning" + ], + "requires": { + "agents": [ + "task-implementor" + ], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "task-researcher": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "rpi", + "research" + ], + "requires": { + "agents": [ + "task-planner" + ], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "task-reviewer": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "rpi", + "review" + ], + "requires": { + "agents": [ + "task-researcher", + "task-planner" + ], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "test-streamlit-dashboard": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "data-science", + "testing" + ], + "requires": { + "agents": [], + "prompts": [], + "instructions": [], + "skills": [] + } + } + }, + "prompts": { + "ado-create-pull-request": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "ado", + "git" + ] + }, + "ado-get-build-info": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "ado" + ] + }, + "ado-get-my-work-items": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "ado" + ] + }, + "ado-process-my-work-items-for-task-planning": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "ado", + "planning" + ] + }, + "ado-update-wit-items": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "ado" + ] + }, + "checkpoint": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "rpi", + "session" + ] + }, + "doc-ops-update": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "documentation" + ] + }, + "git-commit": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "git" + ] + }, + "git-commit-message": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "git" + ] + }, + "git-merge": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "git" + ] + }, + "git-setup": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "git" + ] + }, + "github-add-issue": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github" + ] + }, + "prompt-analyze": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "prompt-engineering" + ] + }, + "prompt-build": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "prompt-engineering" + ] + }, + "prompt-refactor": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "prompt-engineering" + ] + }, + "pull-request": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github", + "git" + ] + }, + "risk-register": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "planning" + ] + }, + "rpi": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "rpi" + ] + }, + "task-implement": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "rpi", + "implementation" + ] + }, + "task-plan": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "rpi", + "planning" + ] + }, + "task-research": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "rpi", + "research" + ] + }, + "task-review": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "rpi", + "review" + ] + } + }, + "instructions": { + "ado-create-pull-request": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "ado", + "git" + ] + }, + "ado-get-build-info": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "ado" + ] + }, + "ado-update-wit-items": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "ado" + ] + }, + "ado-wit-discovery": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "ado" + ] + }, + "ado-wit-planning": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "ado", + "planning" + ] + }, + "commit-message": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "git" + ] + }, + "git-merge": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "git" + ] + }, + "hve-core-location": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "tooling" + ] + }, + "markdown": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "documentation" + ] + }, + "prompt-builder": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "prompt-engineering" + ] + }, + "python-script": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "language" + ] + }, + "uv-projects": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "language" + ] + }, + "writing-style": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "documentation" + ] + }, + "bash/bash": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "language" + ] + }, + "bicep/bicep": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "language", + "infrastructure" + ] + }, + "csharp/csharp": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "language" + ] + }, + "csharp/csharp-tests": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "language", + "testing" + ] + }, + "terraform/terraform": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "language", + "infrastructure" + ] + } + }, + "skills": { + "video-to-gif": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "media", + "tooling" + ] + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 3932b76a..baa9b8dd 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,9 @@ "lint:links": "pwsh -NoProfile -Command \"& scripts/linting/Invoke-LinkLanguageCheck.ps1 -ExcludePaths 'scripts/tests/**'\"", "lint:md-links": "pwsh -File scripts/linting/Markdown-Link-Check.ps1", "lint:frontmatter": "pwsh -NoProfile -Command \"& './scripts/linting/Validate-MarkdownFrontmatter.ps1' -WarningsAsErrors -EnableSchemaValidation\"", + "lint:registry": "pwsh -NoProfile -Command \"& './scripts/linting/Validate-ArtifactRegistry.ps1'\"", "lint:version-consistency": "pwsh -NoProfile -Command \"./scripts/security/Test-ActionVersionConsistency.ps1 -FailOnMismatch\"", - "lint:all": "npm run format:tables && npm run lint:md && npm run lint:ps && npm run lint:yaml && npm run lint:links && npm run lint:frontmatter && npm run lint:version-consistency", + "lint:all": "npm run format:tables && npm run lint:md && npm run lint:ps && npm run lint:yaml && npm run lint:links && npm run lint:frontmatter && npm run lint:registry && npm run lint:version-consistency", "format:tables": "markdown-table-formatter \"**/*.md\"", "extension:prepare": "pwsh ./scripts/extension/Prepare-Extension.ps1", "extension:package": "pwsh ./scripts/extension/Package-Extension.ps1", @@ -36,4 +37,4 @@ }, "author": "Microsoft", "license": "MIT" -} +} \ No newline at end of file diff --git a/scripts/linting/Validate-ArtifactRegistry.ps1 b/scripts/linting/Validate-ArtifactRegistry.ps1 new file mode 100644 index 00000000..76d1bcbd --- /dev/null +++ b/scripts/linting/Validate-ArtifactRegistry.ps1 @@ -0,0 +1,643 @@ +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: MIT + +# Validate-ArtifactRegistry.ps1 +# +# Purpose: Validates the AI Artifacts Registry against its schema and the filesystem +# Author: HVE Core Team + +#Requires -Version 7.0 + +<# +.SYNOPSIS + Validates the AI Artifacts Registry against its schema and the filesystem. + +.DESCRIPTION + Validates the `.github/ai-artifacts-registry.json` file by checking: + - JSON structure and required fields + - Maturity enum values + - Persona ID format and reference validity + - Artifact file existence on disk + - Dependency reference validity + - Orphan file detection (files on disk not in registry) + +.PARAMETER RegistryPath + Path to the registry JSON file. Defaults to `.github/ai-artifacts-registry.json`. + +.PARAMETER RepoRoot + Repository root for resolving artifact file paths. Defaults to script's grandparent. + +.PARAMETER WarningsAsErrors + Treat warnings (orphan files) as errors. + +.PARAMETER OutputPath + Path to write JSON results. Defaults to `logs/registry-validation-results.json`. + +.OUTPUTS + Hashtable with Success bool, Errors array, Warnings array. + +.EXAMPLE + ./Validate-ArtifactRegistry.ps1 + # Validates registry with default paths + +.EXAMPLE + ./Validate-ArtifactRegistry.ps1 -WarningsAsErrors + # Treats orphan file warnings as errors +#> +[CmdletBinding()] +param( + [Parameter()] + [string]$RegistryPath, + + [Parameter()] + [string]$RepoRoot, + + [Parameter()] + [switch]$WarningsAsErrors, + + [Parameter()] + [string]$OutputPath +) + +$ErrorActionPreference = 'Stop' + +# Import CI helpers +Import-Module (Join-Path -Path $PSScriptRoot -ChildPath '../lib/Modules/CIHelpers.psm1') -Force + +#region Validation Functions + +function Test-RegistryStructure { + <# + .SYNOPSIS + Validates the registry JSON structure including required fields. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory)] + [string]$RegistryPath + ) + + $errors = [System.Collections.Generic.List[string]]::new() + + # JSON parse + try { + $content = Get-Content -Path $RegistryPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + } + catch { + $errors.Add("Failed to parse registry JSON: $_") + return @{ Success = $false; Errors = $errors; Registry = $null } + } + + # Required top-level fields + $requiredFields = @('$schema', 'version', 'personas', 'agents', 'prompts', 'instructions', 'skills') + foreach ($field in $requiredFields) { + if (-not $registry.ContainsKey($field)) { + $errors.Add("Missing required field: $field") + } + } + + # Version format + if ($registry.ContainsKey('version') -and $registry['version'] -notmatch '^\d+\.\d+$') { + $errors.Add("Invalid version format: $($registry['version']). Expected: major.minor") + } + + # Personas.definitions + if ($registry.ContainsKey('personas') -and -not $registry['personas'].ContainsKey('definitions')) { + $errors.Add("Missing required field: personas.definitions") + } + + return @{ + Success = ($errors.Count -eq 0) + Errors = $errors + Registry = $registry + } +} + +function Test-PersonaReferences { + <# + .SYNOPSIS + Validates persona definitions and references in artifact entries. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory)] + [hashtable]$Registry + ) + + $errors = [System.Collections.Generic.List[string]]::new() + $definedPersonas = @($Registry['personas']['definitions'].Keys) + + # Validate persona definitions + foreach ($personaId in $definedPersonas) { + if ($personaId -notmatch '^[a-z][a-z0-9-]*$') { + $errors.Add("Invalid persona ID format: $personaId") + } + $persona = $Registry['personas']['definitions'][$personaId] + if (-not $persona.ContainsKey('name') -or [string]::IsNullOrEmpty($persona['name'])) { + $errors.Add("Persona '$personaId' missing 'name' field") + } + if (-not $persona.ContainsKey('description') -or [string]::IsNullOrEmpty($persona['description'])) { + $errors.Add("Persona '$personaId' missing 'description' field") + } + } + + # Validate persona references in artifacts + $sections = @('agents', 'prompts', 'instructions', 'skills') + foreach ($section in $sections) { + foreach ($key in $Registry[$section].Keys) { + $entry = $Registry[$section][$key] + if ($entry.ContainsKey('personas')) { + foreach ($personaRef in $entry['personas']) { + if ($personaRef -notin $definedPersonas) { + $errors.Add("${section}/${key} references undefined persona: $personaRef") + } + } + } + } + } + + return @{ Success = ($errors.Count -eq 0); Errors = $errors } +} + +function Get-ArtifactPath { + <# + .SYNOPSIS + Resolves the file path for an artifact based on section and key. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory)] + [string]$Section, + + [Parameter(Mandatory)] + [string]$Key, + + [Parameter(Mandatory)] + [string]$RepoRoot + ) + + switch ($Section) { + 'agents' { + return Join-Path $RepoRoot ".github/agents/$Key.agent.md" + } + 'prompts' { + return Join-Path $RepoRoot ".github/prompts/$Key.prompt.md" + } + 'instructions' { + return Join-Path $RepoRoot ".github/instructions/$Key.instructions.md" + } + 'skills' { + return Join-Path $RepoRoot ".github/skills/$Key/SKILL.md" + } + default { + return $null + } + } +} + +function Test-ArtifactFileExistence { + <# + .SYNOPSIS + Validates that each artifact key maps to an existing file on disk. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory)] + [hashtable]$Registry, + + [Parameter(Mandatory)] + [string]$RepoRoot + ) + + $errors = [System.Collections.Generic.List[string]]::new() + $sections = @('agents', 'prompts', 'instructions', 'skills') + + foreach ($section in $sections) { + foreach ($key in $Registry[$section].Keys) { + $path = Get-ArtifactPath -Section $section -Key $key -RepoRoot $RepoRoot + if (-not (Test-Path $path)) { + $errors.Add("${section}/${key}: File not found at $path") + } + } + } + + return @{ Success = ($errors.Count -eq 0); Errors = $errors } +} + +function Test-DependencyReferences { + <# + .SYNOPSIS + Validates that dependency references point to existing registry entries. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory)] + [hashtable]$Registry + ) + + $errors = [System.Collections.Generic.List[string]]::new() + $warnings = [System.Collections.Generic.List[string]]::new() + + # Build reference sets + $validAgents = [System.Collections.Generic.HashSet[string]]::new([string[]]@($Registry['agents'].Keys)) + $validPrompts = [System.Collections.Generic.HashSet[string]]::new([string[]]@($Registry['prompts'].Keys)) + $validInstructions = [System.Collections.Generic.HashSet[string]]::new([string[]]@($Registry['instructions'].Keys)) + $validSkills = [System.Collections.Generic.HashSet[string]]::new([string[]]@($Registry['skills'].Keys)) + + # Only agents have requires blocks (by design) + foreach ($agentKey in $Registry['agents'].Keys) { + $agent = $Registry['agents'][$agentKey] + if (-not $agent.ContainsKey('requires')) { continue } + + $requires = $agent['requires'] + + if ($requires.ContainsKey('agents')) { + foreach ($ref in $requires['agents']) { + if (-not $validAgents.Contains($ref)) { + $errors.Add("agents/${agentKey} requires.agents references unknown agent: $ref") + } + } + } + + if ($requires.ContainsKey('prompts')) { + foreach ($ref in $requires['prompts']) { + if (-not $validPrompts.Contains($ref)) { + $errors.Add("agents/${agentKey} requires.prompts references unknown prompt: $ref") + } + } + } + + if ($requires.ContainsKey('instructions')) { + foreach ($ref in $requires['instructions']) { + if (-not $validInstructions.Contains($ref)) { + $errors.Add("agents/${agentKey} requires.instructions references unknown instruction: $ref") + } + } + } + + if ($requires.ContainsKey('skills')) { + foreach ($ref in $requires['skills']) { + if (-not $validSkills.Contains($ref)) { + $errors.Add("agents/${agentKey} requires.skills references unknown skill: $ref") + } + } + } + } + + # Detect circular agent dependencies (warning only) + $circularChains = Find-CircularAgentDependencies -Registry $Registry + foreach ($chain in $circularChains) { + $warnings.Add("Circular agent dependency detected: $($chain -join ' -> ')") + } + + return @{ Success = ($errors.Count -eq 0); Errors = $errors; Warnings = $warnings } +} + +function Find-CircularAgentDependencies { + <# + .SYNOPSIS + Detects circular dependencies in agent requires.agents chains. + #> + [CmdletBinding()] + [OutputType([System.Collections.Generic.List[string[]]])] + param( + [Parameter(Mandatory)] + [hashtable]$Registry + ) + + $chains = [System.Collections.Generic.List[string[]]]::new() + $globalVisited = @{} + + foreach ($agent in $Registry['agents'].Keys) { + $path = [System.Collections.Generic.List[string]]::new() + $localVisited = @{} + Find-CycleFromAgent -Registry $Registry -Agent $agent -Path $path -LocalVisited $localVisited -GlobalVisited $globalVisited -Chains $chains + } + + return $chains +} + +function Find-CycleFromAgent { + param( + [hashtable]$Registry, + [string]$Agent, + [System.Collections.Generic.List[string]]$Path, + [hashtable]$LocalVisited, + [hashtable]$GlobalVisited, + [System.Collections.Generic.List[string[]]]$Chains + ) + + if ($LocalVisited.ContainsKey($Agent)) { + $cycleStart = $Path.IndexOf($Agent) + if ($cycleStart -ge 0) { + $cycle = @($Path[$cycleStart..($Path.Count - 1)]) + @($Agent) + $cycleKey = ($cycle | Sort-Object) -join ',' + if (-not $GlobalVisited.ContainsKey($cycleKey)) { + $GlobalVisited[$cycleKey] = $true + $Chains.Add($cycle) + } + } + return + } + + $LocalVisited[$Agent] = $true + $Path.Add($Agent) + + $entry = $Registry['agents'][$Agent] + if ($entry -and $entry.ContainsKey('requires') -and $entry['requires'].ContainsKey('agents')) { + foreach ($dep in $entry['requires']['agents']) { + if ($Registry['agents'].ContainsKey($dep)) { + Find-CycleFromAgent -Registry $Registry -Agent $dep -Path $Path -LocalVisited $LocalVisited -GlobalVisited $GlobalVisited -Chains $Chains + } + } + } + + $Path.RemoveAt($Path.Count - 1) + $LocalVisited.Remove($Agent) +} + +function Find-OrphanArtifacts { + <# + .SYNOPSIS + Detects artifact files on disk that are not registered in the registry. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory)] + [hashtable]$Registry, + + [Parameter(Mandatory)] + [string]$RepoRoot + ) + + $warnings = [System.Collections.Generic.List[string]]::new() + + # Scan agents + $agentsDir = Join-Path $RepoRoot '.github/agents' + if (Test-Path $agentsDir) { + $agentFiles = Get-ChildItem -Path $agentsDir -Filter '*.agent.md' -File -ErrorAction SilentlyContinue + foreach ($file in $agentFiles) { + $key = $file.BaseName -replace '\.agent$', '' + if (-not $Registry['agents'].ContainsKey($key)) { + $warnings.Add("Orphan agent file not in registry: $($file.FullName)") + } + } + } + + # Scan prompts + $promptsDir = Join-Path $RepoRoot '.github/prompts' + if (Test-Path $promptsDir) { + $promptFiles = Get-ChildItem -Path $promptsDir -Filter '*.prompt.md' -File -Recurse -ErrorAction SilentlyContinue + foreach ($file in $promptFiles) { + $key = $file.BaseName -replace '\.prompt$', '' + if (-not $Registry['prompts'].ContainsKey($key)) { + $warnings.Add("Orphan prompt file not in registry: $($file.FullName)") + } + } + } + + # Scan instructions (including subdirectories) + $instructionsDir = Join-Path $RepoRoot '.github/instructions' + if (Test-Path $instructionsDir) { + $instructionFiles = Get-ChildItem -Path $instructionsDir -Filter '*.instructions.md' -File -Recurse -ErrorAction SilentlyContinue + foreach ($file in $instructionFiles) { + $relativePath = [System.IO.Path]::GetRelativePath($instructionsDir, $file.FullName) -replace '\\', '/' + $key = $relativePath -replace '\.instructions\.md$', '' + if (-not $Registry['instructions'].ContainsKey($key)) { + $warnings.Add("Orphan instruction file not in registry: $($file.FullName)") + } + } + } + + # Scan skills + $skillsDir = Join-Path $RepoRoot '.github/skills' + if (Test-Path $skillsDir) { + $skillDirs = Get-ChildItem -Path $skillsDir -Directory -ErrorAction SilentlyContinue + foreach ($dir in $skillDirs) { + $skillFile = Join-Path $dir.FullName 'SKILL.md' + if (Test-Path $skillFile) { + $key = $dir.Name + if (-not $Registry['skills'].ContainsKey($key)) { + $warnings.Add("Orphan skill directory not in registry: $($dir.FullName)") + } + } + } + } + + return @{ Warnings = $warnings } +} + +#endregion Validation Functions + +#region Output Functions + +function Write-RegistryValidationOutput { + <# + .SYNOPSIS + Writes validation results to console with formatting. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [hashtable]$Result, + + [Parameter(Mandatory)] + [string]$RegistryPath + ) + + Write-Host "`n๐Ÿ” Registry Validation Results" -ForegroundColor Cyan + Write-Host "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" -ForegroundColor DarkGray + Write-Host " Registry: $RegistryPath" + + if ($Result.Errors.Count -gt 0) { + Write-Host "`nโŒ Errors ($($Result.Errors.Count)):" -ForegroundColor Red + foreach ($errorItem in $Result.Errors) { + Write-Host " โ€ข $errorItem" -ForegroundColor Red + } + } + + if ($Result.Warnings.Count -gt 0) { + Write-Host "`nโš ๏ธ Warnings ($($Result.Warnings.Count)):" -ForegroundColor Yellow + foreach ($warningItem in $Result.Warnings) { + Write-Host " โ€ข $warningItem" -ForegroundColor Yellow + } + } + + Write-Host "`n๐Ÿ“Š Summary:" -ForegroundColor Cyan + $errorColor = if ($Result.Errors.Count -gt 0) { 'Red' } else { 'Green' } + $warnColor = if ($Result.Warnings.Count -gt 0) { 'Yellow' } else { 'Green' } + Write-Host " Errors: $($Result.Errors.Count)" -ForegroundColor $errorColor + Write-Host " Warnings: $($Result.Warnings.Count)" -ForegroundColor $warnColor + + if ($Result.ArtifactCounts) { + Write-Host " Agents: $($Result.ArtifactCounts.Agents)" + Write-Host " Prompts: $($Result.ArtifactCounts.Prompts)" + Write-Host " Instructions: $($Result.ArtifactCounts.Instructions)" + Write-Host " Skills: $($Result.ArtifactCounts.Skills)" + } +} + +function Export-RegistryValidationResults { + <# + .SYNOPSIS + Exports validation results to JSON file. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [hashtable]$Result, + + [Parameter(Mandatory)] + [string]$OutputPath + ) + + $outputDir = Split-Path -Path $OutputPath -Parent + if ($outputDir -and -not (Test-Path $outputDir)) { + New-Item -ItemType Directory -Path $outputDir -Force | Out-Null + } + + $exportData = @{ + timestamp = (Get-Date -Format 'o') + success = $Result.Success + errors = $Result.Errors + warnings = $Result.Warnings + artifactCounts = $Result.ArtifactCounts + } + + $exportData | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath -Encoding utf8 +} + +#endregion Output Functions + +#region Main Execution + +try { + if ($MyInvocation.InvocationName -ne '.') { + # Resolve paths + if (-not $RepoRoot) { + $RepoRoot = $PSScriptRoot + while ($RepoRoot -and -not (Test-Path (Join-Path $RepoRoot '.git'))) { + $RepoRoot = Split-Path -Parent $RepoRoot + } + if (-not $RepoRoot) { + throw "Could not find repository root" + } + } + + if (-not $RegistryPath) { + $RegistryPath = Join-Path $RepoRoot '.github/ai-artifacts-registry.json' + } + + if (-not $OutputPath) { + $OutputPath = Join-Path $RepoRoot 'logs/registry-validation-results.json' + } + + Write-Host "๐Ÿ” Validating AI Artifacts Registry..." -ForegroundColor Cyan + + # Validate file exists + if (-not (Test-Path $RegistryPath)) { + throw "Registry file not found: $RegistryPath" + } + + # Run validations + $allErrors = [System.Collections.Generic.List[string]]::new() + $allWarnings = [System.Collections.Generic.List[string]]::new() + + # Step 1: Structure validation + $structureResult = Test-RegistryStructure -RegistryPath $RegistryPath + $allErrors.AddRange($structureResult.Errors) + + if (-not $structureResult.Success) { + # Cannot continue without valid structure + $result = @{ + Success = $false + Errors = $allErrors + Warnings = $allWarnings + ArtifactCounts = $null + } + } + else { + $registry = $structureResult.Registry + + # Step 2: Persona references + $personaResult = Test-PersonaReferences -Registry $registry + $allErrors.AddRange($personaResult.Errors) + + # Step 3: File existence + $fileResult = Test-ArtifactFileExistence -Registry $registry -RepoRoot $RepoRoot + $allErrors.AddRange($fileResult.Errors) + + # Step 4: Dependency references + $depResult = Test-DependencyReferences -Registry $registry + $allErrors.AddRange($depResult.Errors) + $allWarnings.AddRange($depResult.Warnings) + + # Step 5: Orphan detection + $orphanResult = Find-OrphanArtifacts -Registry $registry -RepoRoot $RepoRoot + $allWarnings.AddRange($orphanResult.Warnings) + + # Build result + $result = @{ + Success = ($allErrors.Count -eq 0) + Errors = $allErrors + Warnings = $allWarnings + ArtifactCounts = @{ + Agents = $registry['agents'].Count + Prompts = $registry['prompts'].Count + Instructions = $registry['instructions'].Count + Skills = $registry['skills'].Count + } + } + } + + # Output + Write-RegistryValidationOutput -Result $result -RegistryPath $RegistryPath + + # CI annotations + if (Test-CIEnvironment) { + foreach ($errItem in $result.Errors) { + Write-CIAnnotation -Message $errItem -Level Error -File $RegistryPath + } + foreach ($warnItem in $result.Warnings) { + Write-CIAnnotation -Message $warnItem -Level Warning -File $RegistryPath + } + } + + # Export results + Export-RegistryValidationResults -Result $result -OutputPath $OutputPath + + # Exit code + $exitCode = 0 + if ($result.Errors.Count -gt 0) { + $exitCode = 1 + } + elseif ($WarningsAsErrors -and $result.Warnings.Count -gt 0) { + $exitCode = 1 + } + + if ($exitCode -eq 0) { + Write-Host "`nโœ… Registry validation passed!" -ForegroundColor Green + } + else { + Write-Host "`nโŒ Registry validation failed!" -ForegroundColor Red + } + + exit $exitCode + } +} +catch { + Write-Error -ErrorAction Continue "Registry validation failed: $($_.Exception.Message)" + if (Test-CIEnvironment) { + Write-CIAnnotation -Message "Registry validation failed: $($_.Exception.Message)" -Level Error + } + exit 1 +} + +#endregion diff --git a/scripts/linting/schemas/ai-artifacts-registry.schema.json b/scripts/linting/schemas/ai-artifacts-registry.schema.json new file mode 100644 index 00000000..e5db7d54 --- /dev/null +++ b/scripts/linting/schemas/ai-artifacts-registry.schema.json @@ -0,0 +1,195 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/microsoft/hve-core/schemas/ai-artifacts-registry.schema.json", + "title": "AI Artifacts Registry Schema", + "description": "Schema for the centralized AI artifacts registry containing persona tags, maturity levels, dependency mappings, and metadata for all HVE-Core artifacts", + "type": "object", + "required": [ + "$schema", + "version", + "personas", + "agents", + "prompts", + "instructions", + "skills" + ], + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema reference for validation" + }, + "version": { + "type": "string", + "pattern": "^\\d+\\.\\d+$", + "description": "Registry format version (semver major.minor)" + }, + "personas": { + "type": "object", + "required": [ + "definitions" + ], + "properties": { + "definitions": { + "type": "object", + "description": "Persona ID to metadata mapping", + "additionalProperties": { + "type": "object", + "required": [ + "name", + "description" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "agents": { + "type": "object", + "description": "Agent artifact entries keyed by agent name", + "additionalProperties": { + "$ref": "#/$defs/agentEntry" + } + }, + "prompts": { + "type": "object", + "description": "Prompt artifact entries keyed by prompt name", + "additionalProperties": { + "$ref": "#/$defs/simpleArtifactEntry" + } + }, + "instructions": { + "type": "object", + "description": "Instruction artifact entries keyed by instruction name (use dir/name for subdirectory files)", + "additionalProperties": { + "$ref": "#/$defs/simpleArtifactEntry" + } + }, + "skills": { + "type": "object", + "description": "Skill artifact entries keyed by skill directory name", + "additionalProperties": { + "$ref": "#/$defs/simpleArtifactEntry" + } + } + }, + "additionalProperties": false, + "$defs": { + "agentEntry": { + "type": "object", + "required": [ + "maturity", + "personas", + "tags" + ], + "properties": { + "maturity": { + "type": "string", + "enum": [ + "stable", + "preview", + "experimental", + "deprecated" + ], + "description": "Maturity level for channel-based filtering" + }, + "personas": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "description": "Persona IDs this artifact belongs to. Empty array means universal (all personas)." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Freeform categorization tags" + }, + "requires": { + "type": "object", + "properties": { + "agents": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Agent names this artifact depends on (e.g., handoff targets)" + }, + "prompts": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Prompt names this artifact depends on" + }, + "instructions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Instruction names this artifact depends on" + }, + "skills": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Skill names this artifact depends on" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "simpleArtifactEntry": { + "type": "object", + "required": [ + "maturity", + "personas", + "tags" + ], + "properties": { + "maturity": { + "type": "string", + "enum": [ + "stable", + "preview", + "experimental", + "deprecated" + ], + "description": "Maturity level for channel-based filtering" + }, + "personas": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "description": "Persona IDs this artifact belongs to. Empty array means universal (all personas)." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Freeform categorization tags" + } + }, + "additionalProperties": false + } + } +} \ No newline at end of file From 5426cd75f739c9aa918bf685eac39d9972714447 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 11:00:06 +0100 Subject: [PATCH 02/62] test(scripts): add tests for Validate-ArtifactRegistry.ps1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add unit tests for registry structure, persona refs, and dependency validation - add circular dependency detection and orphan artifact tests - add integration tests for CI environment handling and exit codes ๐Ÿงช - Generated by Copilot --- .../ArtifactRegistry/circular-deps.json | 62 ++ .../ArtifactRegistry/invalid-json.json | 4 + .../ArtifactRegistry/invalid-persona-id.json | 20 + .../ArtifactRegistry/invalid-version.json | 16 + .../ArtifactRegistry/missing-fields.json | 3 + .../missing-persona-name.json | 15 + .../missing-personas-defs.json | 11 + .../ArtifactRegistry/no-requires.json | 26 + .../undefined-persona-ref.json | 39 + .../ArtifactRegistry/unknown-dep-refs.json | 38 + .../ArtifactRegistry/valid-registry.json | 90 ++ .../Validate-ArtifactRegistry.Tests.ps1 | 919 ++++++++++++++++++ 12 files changed, 1243 insertions(+) create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/missing-fields.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/missing-persona-name.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/missing-personas-defs.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/no-requires.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json create mode 100644 scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 diff --git a/scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json b/scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json new file mode 100644 index 00000000..53a48f5d --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json @@ -0,0 +1,62 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "developer": { + "name": "Developer", + "description": "Software engineers" + } + } + }, + "agents": { + "agent-a": { + "maturity": "stable", + "personas": [ + "developer" + ], + "tags": [], + "requires": { + "agents": [ + "agent-b" + ], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "agent-b": { + "maturity": "stable", + "personas": [ + "developer" + ], + "tags": [], + "requires": { + "agents": [ + "agent-c" + ], + "prompts": [], + "instructions": [], + "skills": [] + } + }, + "agent-c": { + "maturity": "stable", + "personas": [ + "developer" + ], + "tags": [], + "requires": { + "agents": [ + "agent-a" + ], + "prompts": [], + "instructions": [], + "skills": [] + } + } + }, + "prompts": {}, + "instructions": {}, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json new file mode 100644 index 00000000..c3b43c22 --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json @@ -0,0 +1,4 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0""malformed JSON - missing comma after version" +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json new file mode 100644 index 00000000..bdc78993 --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json @@ -0,0 +1,20 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "Invalid_ID_Format": { + "name": "Invalid Persona", + "description": "This persona ID uses uppercase and underscores" + }, + "123-starts-with-number": { + "name": "Bad Start", + "description": "This persona ID starts with a number" + } + } + }, + "agents": {}, + "prompts": {}, + "instructions": {}, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json new file mode 100644 index 00000000..a5050e66 --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json @@ -0,0 +1,16 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0.0", + "personas": { + "definitions": { + "developer": { + "name": "Developer", + "description": "Software engineers" + } + } + }, + "agents": {}, + "prompts": {}, + "instructions": {}, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/missing-fields.json b/scripts/tests/Fixtures/ArtifactRegistry/missing-fields.json new file mode 100644 index 00000000..9f15a245 --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/missing-fields.json @@ -0,0 +1,3 @@ +{ + "description": "Registry missing required top-level fields" +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/missing-persona-name.json b/scripts/tests/Fixtures/ArtifactRegistry/missing-persona-name.json new file mode 100644 index 00000000..abdeeb8a --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/missing-persona-name.json @@ -0,0 +1,15 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "incomplete-persona": { + "description": "This persona is missing the name field" + } + } + }, + "agents": {}, + "prompts": {}, + "instructions": {}, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/missing-personas-defs.json b/scripts/tests/Fixtures/ArtifactRegistry/missing-personas-defs.json new file mode 100644 index 00000000..a744e750 --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/missing-personas-defs.json @@ -0,0 +1,11 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "note": "Missing definitions key" + }, + "agents": {}, + "prompts": {}, + "instructions": {}, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/no-requires.json b/scripts/tests/Fixtures/ArtifactRegistry/no-requires.json new file mode 100644 index 00000000..9d123960 --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/no-requires.json @@ -0,0 +1,26 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "developer": { + "name": "Developer", + "description": "Software engineers" + } + } + }, + "agents": { + "standalone-agent": { + "maturity": "stable", + "personas": [ + "developer" + ], + "tags": [ + "standalone" + ] + } + }, + "prompts": {}, + "instructions": {}, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json b/scripts/tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json new file mode 100644 index 00000000..80ea9961 --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json @@ -0,0 +1,39 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "developer": { + "name": "Developer", + "description": "Software engineers" + } + } + }, + "agents": { + "bad-agent": { + "maturity": "stable", + "personas": [ + "nonexistent-persona", + "also-undefined" + ], + "tags": [], + "requires": { + "agents": [], + "prompts": [], + "instructions": [], + "skills": [] + } + } + }, + "prompts": { + "bad-prompt": { + "maturity": "stable", + "personas": [ + "undefined-persona" + ], + "tags": [] + } + }, + "instructions": {}, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json b/scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json new file mode 100644 index 00000000..3231b117 --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json @@ -0,0 +1,38 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "developer": { + "name": "Developer", + "description": "Software engineers" + } + } + }, + "agents": { + "broken-agent": { + "maturity": "stable", + "personas": [ + "developer" + ], + "tags": [], + "requires": { + "agents": [ + "nonexistent-agent" + ], + "prompts": [ + "nonexistent-prompt" + ], + "instructions": [ + "nonexistent-instruction" + ], + "skills": [ + "nonexistent-skill" + ] + } + } + }, + "prompts": {}, + "instructions": {}, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json b/scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json new file mode 100644 index 00000000..5fcb0b26 --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json @@ -0,0 +1,90 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "developer": { + "name": "Developer", + "description": "Software engineers building applications" + }, + "devops": { + "name": "DevOps Engineer", + "description": "Infrastructure and deployment specialists" + } + } + }, + "agents": { + "test-agent": { + "maturity": "stable", + "personas": [ + "developer" + ], + "tags": [ + "test" + ], + "requires": { + "agents": [], + "prompts": [ + "test-prompt" + ], + "instructions": [ + "test-instruction" + ], + "skills": [] + } + }, + "dependent-agent": { + "maturity": "stable", + "personas": [ + "developer", + "devops" + ], + "tags": [ + "test" + ], + "requires": { + "agents": [ + "test-agent" + ], + "prompts": [], + "instructions": [], + "skills": [ + "test-skill" + ] + } + } + }, + "prompts": { + "test-prompt": { + "maturity": "stable", + "personas": [ + "developer" + ], + "tags": [ + "test" + ] + } + }, + "instructions": { + "test-instruction": { + "maturity": "stable", + "personas": [ + "developer" + ], + "tags": [ + "test" + ] + } + }, + "skills": { + "test-skill": { + "maturity": "stable", + "personas": [ + "devops" + ], + "tags": [ + "test" + ] + } + } +} \ No newline at end of file diff --git a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 new file mode 100644 index 00000000..dd3770da --- /dev/null +++ b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 @@ -0,0 +1,919 @@ +#Requires -Modules Pester +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: MIT + +BeforeAll { + # Dot-source the main script to make functions available + $scriptPath = Join-Path $PSScriptRoot '../../linting/Validate-ArtifactRegistry.ps1' + . $scriptPath + + # Import CI helpers module + $ciHelpersPath = Join-Path $PSScriptRoot '../../lib/Modules/CIHelpers.psm1' + Import-Module $ciHelpersPath -Force + + # Set up fixture paths + $script:FixtureDir = Join-Path $PSScriptRoot '../Fixtures/ArtifactRegistry' + $script:RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../../..')).Path + + # Fixture file paths + $script:ValidRegistryPath = Join-Path $script:FixtureDir 'valid-registry.json' + $script:InvalidJsonPath = Join-Path $script:FixtureDir 'invalid-json.json' + $script:MissingFieldsPath = Join-Path $script:FixtureDir 'missing-fields.json' + $script:InvalidVersionPath = Join-Path $script:FixtureDir 'invalid-version.json' + $script:MissingPersonasDefsPath = Join-Path $script:FixtureDir 'missing-personas-defs.json' + $script:InvalidPersonaIdPath = Join-Path $script:FixtureDir 'invalid-persona-id.json' + $script:MissingPersonaNamePath = Join-Path $script:FixtureDir 'missing-persona-name.json' + $script:UndefinedPersonaRefPath = Join-Path $script:FixtureDir 'undefined-persona-ref.json' + $script:UnknownDepRefsPath = Join-Path $script:FixtureDir 'unknown-dep-refs.json' + $script:CircularDepsPath = Join-Path $script:FixtureDir 'circular-deps.json' + $script:NoRequiresPath = Join-Path $script:FixtureDir 'no-requires.json' +} + +#region Test-RegistryStructure Tests + +Describe 'Test-RegistryStructure' -Tag 'Unit' { + Context 'JSON parsing' { + It 'Returns error when JSON is malformed' { + $result = Test-RegistryStructure -RegistryPath $script:InvalidJsonPath + $result.Success | Should -BeFalse + $result.Errors[0] | Should -Match 'Failed to parse registry JSON' + $result.Registry | Should -BeNull + } + + It 'Parses valid JSON successfully' { + $result = Test-RegistryStructure -RegistryPath $script:ValidRegistryPath + $result.Success | Should -BeTrue + $result.Registry | Should -Not -BeNull + } + } + + Context 'Required fields validation' { + It 'Reports missing $schema field' { + $result = Test-RegistryStructure -RegistryPath $script:MissingFieldsPath + $result.Success | Should -BeFalse + $result.Errors | Should -Contain 'Missing required field: $schema' + } + + It 'Reports all missing required fields' { + $result = Test-RegistryStructure -RegistryPath $script:MissingFieldsPath + # Should report missing: $schema, version, personas, agents, prompts, instructions, skills + $result.Errors.Count | Should -BeGreaterOrEqual 6 + } + } + + Context 'Version format validation' { + It 'Reports invalid version format' { + $result = Test-RegistryStructure -RegistryPath $script:InvalidVersionPath + $result.Errors | Should -Contain 'Invalid version format: 1.0.0. Expected: major.minor' + } + + It 'Accepts valid version format' { + $result = Test-RegistryStructure -RegistryPath $script:ValidRegistryPath + $result.Errors | Where-Object { $_ -match 'version format' } | Should -BeNullOrEmpty + } + } + + Context 'Personas structure' { + It 'Reports missing personas.definitions' { + $result = Test-RegistryStructure -RegistryPath $script:MissingPersonasDefsPath + $result.Errors | Should -Contain 'Missing required field: personas.definitions' + } + } +} + +#endregion + +#region Test-PersonaReferences Tests + +Describe 'Test-PersonaReferences' -Tag 'Unit' { + BeforeAll { + # Load valid registry for reference + $content = Get-Content $script:ValidRegistryPath -Raw + $script:ValidRegistry = $content | ConvertFrom-Json -AsHashtable + } + + Context 'Persona definition validation' { + It 'Reports invalid persona ID format' { + $content = Get-Content $script:InvalidPersonaIdPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-PersonaReferences -Registry $registry + $result.Errors | Where-Object { $_ -match 'Invalid persona ID format' } | Should -Not -BeNullOrEmpty + } + + It 'Reports missing name field' { + $content = Get-Content $script:MissingPersonaNamePath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-PersonaReferences -Registry $registry + $result.Errors | Where-Object { $_ -match "missing 'name' field" } | Should -Not -BeNullOrEmpty + } + + It 'Reports missing description field' { + # Create registry with persona missing description + $registry = @{ + personas = @{ + definitions = @{ + 'test-persona' = @{ name = 'Test' } + } + } + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Test-PersonaReferences -Registry $registry + $result.Errors | Where-Object { $_ -match "missing 'description' field" } | Should -Not -BeNullOrEmpty + } + + It 'Passes with valid persona definitions' { + $result = Test-PersonaReferences -Registry $script:ValidRegistry + $result.Success | Should -BeTrue + } + } + + Context 'Persona reference validation' { + It 'Reports undefined persona references in artifacts' { + $content = Get-Content $script:UndefinedPersonaRefPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-PersonaReferences -Registry $registry + $result.Errors | Where-Object { $_ -match 'references undefined persona' } | Should -Not -BeNullOrEmpty + } + } +} + +#endregion + +#region Get-ArtifactPath Tests + +Describe 'Get-ArtifactPath' -Tag 'Unit' { + BeforeAll { + $script:TestRepoRoot = '/test/repo' + } + + Context 'Section path mapping' { + It 'Returns correct path for agents section' { + $result = Get-ArtifactPath -Section 'agents' -Key 'test-agent' -RepoRoot $script:TestRepoRoot + $result | Should -Be '/test/repo/.github/agents/test-agent.agent.md' + } + + It 'Returns correct path for prompts section' { + $result = Get-ArtifactPath -Section 'prompts' -Key 'test-prompt' -RepoRoot $script:TestRepoRoot + $result | Should -Be '/test/repo/.github/prompts/test-prompt.prompt.md' + } + + It 'Returns correct path for instructions section' { + $result = Get-ArtifactPath -Section 'instructions' -Key 'test-instruction' -RepoRoot $script:TestRepoRoot + $result | Should -Be '/test/repo/.github/instructions/test-instruction.instructions.md' + } + + It 'Returns correct path for skills section' { + $result = Get-ArtifactPath -Section 'skills' -Key 'test-skill' -RepoRoot $script:TestRepoRoot + $result | Should -Be '/test/repo/.github/skills/test-skill/SKILL.md' + } + } + + Context 'Unknown section handling' { + It 'Returns null for unknown section' { + $result = Get-ArtifactPath -Section 'unknown' -Key 'test' -RepoRoot $script:TestRepoRoot + $result | Should -BeNull + } + } +} + +#endregion + +#region Test-ArtifactFileExistence Tests + +Describe 'Test-ArtifactFileExistence' -Tag 'Unit' { + Context 'File existence checks' { + It 'Returns success when all files exist' { + Mock Test-Path { return $true } + $registry = @{ + agents = @{ 'existing-agent' = @{} } + prompts = @{ 'existing-prompt' = @{} } + instructions = @{ 'existing-instruction' = @{} } + skills = @{ 'existing-skill' = @{} } + } + $result = Test-ArtifactFileExistence -Registry $registry -RepoRoot $TestDrive + $result.Success | Should -BeTrue + $result.Errors.Count | Should -Be 0 + } + + It 'Returns error for missing file' { + Mock Test-Path { return $false } + $registry = @{ + agents = @{ 'missing-agent' = @{} } + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Test-ArtifactFileExistence -Registry $registry -RepoRoot $TestDrive + $result.Success | Should -BeFalse + $result.Errors[0] | Should -Match 'agents/missing-agent: File not found' + } + } +} + +#endregion + +#region Test-DependencyReferences Tests + +Describe 'Test-DependencyReferences' -Tag 'Unit' { + BeforeAll { + $script:BaseRegistry = @{ + agents = @{ + 'agent-a' = @{ + requires = @{ + agents = @() + prompts = @() + instructions = @() + skills = @() + } + } + } + prompts = @{ 'prompt-a' = @{} } + instructions = @{ 'instruction-a' = @{} } + skills = @{ 'skill-a' = @{} } + } + } + + Context 'Dependency validation' { + It 'Reports unknown agent reference' { + $content = Get-Content $script:UnknownDepRefsPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-DependencyReferences -Registry $registry + $result.Errors | Where-Object { $_ -match 'requires.agents references unknown agent' } | Should -Not -BeNullOrEmpty + } + + It 'Reports unknown prompt reference' { + $content = Get-Content $script:UnknownDepRefsPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-DependencyReferences -Registry $registry + $result.Errors | Where-Object { $_ -match 'requires.prompts references unknown prompt' } | Should -Not -BeNullOrEmpty + } + + It 'Reports unknown instruction reference' { + $content = Get-Content $script:UnknownDepRefsPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-DependencyReferences -Registry $registry + $result.Errors | Where-Object { $_ -match 'requires.instructions references unknown instruction' } | Should -Not -BeNullOrEmpty + } + + It 'Reports unknown skill reference' { + $content = Get-Content $script:UnknownDepRefsPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-DependencyReferences -Registry $registry + $result.Errors | Where-Object { $_ -match 'requires.skills references unknown skill' } | Should -Not -BeNullOrEmpty + } + + It 'Passes with valid references' { + $registry = @{ + agents = @{ + 'agent-a' = @{ + requires = @{ + agents = @() + prompts = @('prompt-a') + instructions = @() + skills = @() + } + } + } + prompts = @{ 'prompt-a' = @{} } + instructions = @{} + skills = @{} + } + $result = Test-DependencyReferences -Registry $registry + $result.Success | Should -BeTrue + } + + It 'Skips agents without requires block' { + $content = Get-Content $script:NoRequiresPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-DependencyReferences -Registry $registry + $result.Success | Should -BeTrue + } + } + + Context 'Circular dependency detection' { + It 'Returns warnings for circular dependencies' { + $content = Get-Content $script:CircularDepsPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-DependencyReferences -Registry $registry + $result.Warnings.Count | Should -BeGreaterThan 0 + $result.Warnings[0] | Should -Match 'Circular agent dependency detected' + } + + It 'Success remains true with circular warnings' { + $content = Get-Content $script:CircularDepsPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-DependencyReferences -Registry $registry + # Circular deps are warnings, not errors + $result.Success | Should -BeTrue + } + } +} + +#endregion + +#region Find-CircularAgentDependencies Tests + +Describe 'Find-CircularAgentDependencies' -Tag 'Unit' { + Context 'Cycle detection' { + It 'Returns empty list when no cycles exist' { + $registry = @{ + agents = @{ + 'agent-a' = @{ requires = @{ agents = @('agent-b') } } + 'agent-b' = @{ requires = @{ agents = @() } } + } + } + $result = Find-CircularAgentDependencies -Registry $registry + $result.Count | Should -Be 0 + } + + It 'Detects simple A -> B -> A cycle' { + $registry = @{ + agents = @{ + 'agent-a' = @{ requires = @{ agents = @('agent-b') } } + 'agent-b' = @{ requires = @{ agents = @('agent-a') } } + } + } + $result = Find-CircularAgentDependencies -Registry $registry + $result.Count | Should -BeGreaterThan 0 + } + + It 'Detects A -> B -> C -> A cycle' { + $content = Get-Content $script:CircularDepsPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Find-CircularAgentDependencies -Registry $registry + $result.Count | Should -BeGreaterThan 0 + } + + It 'Does not report non-cyclic paths' { + $registry = @{ + agents = @{ + 'agent-a' = @{ requires = @{ agents = @('agent-b', 'agent-c') } } + 'agent-b' = @{ requires = @{ agents = @('agent-d') } } + 'agent-c' = @{ requires = @{ agents = @('agent-d') } } + 'agent-d' = @{ requires = @{ agents = @() } } + } + } + $result = Find-CircularAgentDependencies -Registry $registry + $result.Count | Should -Be 0 + } + } +} + +#endregion + +#region Find-CycleFromAgent Tests + +Describe 'Find-CycleFromAgent' -Tag 'Unit' { + Context 'Recursive cycle detection' { + It 'Handles agent with no requires.agents' { + $registry = @{ + agents = @{ + 'agent-a' = @{ requires = @{ prompts = @() } } # no agents key + } + } + $result = Find-CircularAgentDependencies -Registry $registry + $result.Count | Should -Be 0 + } + + It 'Skips references to nonexistent agents' { + $registry = @{ + agents = @{ + 'agent-a' = @{ requires = @{ agents = @('nonexistent') } } + } + } + $result = Find-CircularAgentDependencies -Registry $registry + $result.Count | Should -Be 0 + } + + It 'Handles self-referencing agent' { + $registry = @{ + agents = @{ + 'agent-a' = @{ requires = @{ agents = @('agent-a') } } + } + } + $result = Find-CircularAgentDependencies -Registry $registry + $result.Count | Should -BeGreaterThan 0 + } + + It 'Deduplicates equivalent cycles using global visited tracking' { + $registry = @{ + agents = @{ + 'agent-a' = @{ requires = @{ agents = @('agent-b') } } + 'agent-b' = @{ requires = @{ agents = @('agent-a') } } + } + } + $result = Find-CircularAgentDependencies -Registry $registry + # The function may report cycles from different starting points + # but the global visited hash prevents truly identical cycles + $result.Count | Should -BeGreaterThan 0 + # Verify cycles are detected + ($result | ForEach-Object { $_ -join ',' }) | Should -Match 'agent-a|agent-b' + } + + It 'Detects multiple independent cycles' { + $registry = @{ + agents = @{ + 'agent-a' = @{ requires = @{ agents = @('agent-b') } } + 'agent-b' = @{ requires = @{ agents = @('agent-a') } } + 'agent-c' = @{ requires = @{ agents = @('agent-d') } } + 'agent-d' = @{ requires = @{ agents = @('agent-c') } } + } + } + $result = Find-CircularAgentDependencies -Registry $registry + $result.Count | Should -BeGreaterOrEqual 2 + } + } +} + +#endregion + +#region Find-OrphanArtifacts Tests + +Describe 'Find-OrphanArtifacts' -Tag 'Unit' { + BeforeAll { + $script:OrphanTestRoot = Join-Path $TestDrive 'orphan-test-repo' + } + + BeforeEach { + # Clean and recreate test directory structure + if (Test-Path $script:OrphanTestRoot) { + Remove-Item -Path $script:OrphanTestRoot -Recurse -Force + } + New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/agents" -Force | Out-Null + New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/prompts" -Force | Out-Null + New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/instructions" -Force | Out-Null + New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/skills" -Force | Out-Null + } + + Context 'Orphan detection by section' { + It 'Returns empty warnings when no orphans exist' { + $registry = @{ + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot + $result.Warnings.Count | Should -Be 0 + } + + It 'Detects orphan agent file' { + Set-Content -Path "$script:OrphanTestRoot/.github/agents/orphan-agent.agent.md" -Value '# Orphan' + $registry = @{ + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot + $result.Warnings | Where-Object { $_ -match 'Orphan agent file' } | Should -Not -BeNullOrEmpty + } + + It 'Detects orphan prompt file' { + Set-Content -Path "$script:OrphanTestRoot/.github/prompts/orphan-prompt.prompt.md" -Value '# Orphan' + $registry = @{ + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot + $result.Warnings | Where-Object { $_ -match 'Orphan prompt file' } | Should -Not -BeNullOrEmpty + } + + It 'Detects orphan instruction file in subdirectory' { + New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/instructions/subdir" -Force | Out-Null + Set-Content -Path "$script:OrphanTestRoot/.github/instructions/subdir/orphan.instructions.md" -Value '# Orphan' + $registry = @{ + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot + $result.Warnings | Where-Object { $_ -match 'Orphan instruction file' } | Should -Not -BeNullOrEmpty + } + + It 'Detects orphan skill directory' { + New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/skills/orphan-skill" -Force | Out-Null + Set-Content -Path "$script:OrphanTestRoot/.github/skills/orphan-skill/SKILL.md" -Value '# Orphan Skill' + $registry = @{ + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot + $result.Warnings | Where-Object { $_ -match 'Orphan skill directory' } | Should -Not -BeNullOrEmpty + } + } + + Context 'Missing directories' { + It 'Handles missing artifact directories gracefully' { + $emptyRepoRoot = Join-Path $TestDrive 'empty-repo' + New-Item -ItemType Directory -Path $emptyRepoRoot -Force | Out-Null + + $registry = @{ + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $emptyRepoRoot + $result.Warnings.Count | Should -Be 0 + } + } +} + +#endregion + +#region Write-RegistryValidationOutput Tests + +Describe 'Write-RegistryValidationOutput' -Tag 'Unit' { + Context 'Console output formatting' { + It 'Outputs errors section when errors exist' { + $result = @{ + Errors = @('Error 1', 'Error 2') + Warnings = @() + ArtifactCounts = $null + } + + # Capture Write-Host output using 6>&1 + $output = Write-RegistryValidationOutput -Result $result -RegistryPath '/test/registry.json' 6>&1 + $outputText = $output -join "`n" + $outputText | Should -Match 'Errors \(2\)' + } + + It 'Outputs warnings section when warnings exist' { + $result = @{ + Errors = @() + Warnings = @('Warning 1') + ArtifactCounts = $null + } + + $output = Write-RegistryValidationOutput -Result $result -RegistryPath '/test/registry.json' 6>&1 + $outputText = $output -join "`n" + $outputText | Should -Match 'Warnings \(1\)' + } + + It 'Outputs clean summary without errors or warnings' { + $result = @{ + Errors = @() + Warnings = @() + ArtifactCounts = $null + } + + $output = Write-RegistryValidationOutput -Result $result -RegistryPath '/test/registry.json' 6>&1 + $outputText = $output -join "`n" + $outputText | Should -Match 'Errors: 0' + } + + It 'Outputs artifact counts when provided' { + $result = @{ + Errors = @() + Warnings = @() + ArtifactCounts = @{ + Agents = 10 + Prompts = 5 + Instructions = 8 + Skills = 2 + } + } + + $output = Write-RegistryValidationOutput -Result $result -RegistryPath '/test/registry.json' 6>&1 + $outputText = $output -join "`n" + $outputText | Should -Match 'Agents: 10' + $outputText | Should -Match 'Prompts: 5' + $outputText | Should -Match 'Instructions: 8' + $outputText | Should -Match 'Skills: 2' + } + } +} + +#endregion + +#region Export-RegistryValidationResults Tests + +Describe 'Export-RegistryValidationResults' -Tag 'Unit' { + Context 'JSON export' { + It 'Creates output directory if missing' { + $outputPath = Join-Path $TestDrive 'new-dir/results.json' + $result = @{ + Success = $true + Errors = @() + Warnings = @() + ArtifactCounts = $null + } + + Export-RegistryValidationResults -Result $result -OutputPath $outputPath + + Test-Path (Split-Path $outputPath -Parent) | Should -BeTrue + } + + It 'Uses existing output directory' { + $existingDir = Join-Path $TestDrive 'existing-dir' + New-Item -ItemType Directory -Path $existingDir -Force | Out-Null + $outputPath = Join-Path $existingDir 'results.json' + $result = @{ + Success = $true + Errors = @() + Warnings = @() + ArtifactCounts = $null + } + + Export-RegistryValidationResults -Result $result -OutputPath $outputPath + + Test-Path $outputPath | Should -BeTrue + } + + It 'Writes correct JSON structure' { + $outputPath = Join-Path $TestDrive 'structure-test.json' + $result = @{ + Success = $true + Errors = @('error1') + Warnings = @('warning1') + ArtifactCounts = @{ Agents = 5 } + } + + Export-RegistryValidationResults -Result $result -OutputPath $outputPath + + $exported = Get-Content $outputPath -Raw | ConvertFrom-Json + $exported.success | Should -BeTrue + $exported.errors | Should -Contain 'error1' + $exported.warnings | Should -Contain 'warning1' + $exported.timestamp | Should -Not -BeNullOrEmpty + $exported.artifactCounts.Agents | Should -Be 5 + } + } +} + +#endregion + +#region Main Execution Block Tests + +Describe 'Main Execution Block' -Tag 'Integration' { + BeforeAll { + # Save original environment + $script:OriginalGHA = $env:GITHUB_ACTIONS + $script:OriginalTFBuild = $env:TF_BUILD + $script:MainScriptPath = Join-Path $PSScriptRoot '../../linting/Validate-ArtifactRegistry.ps1' + } + + AfterAll { + # Restore original environment + if ($null -eq $script:OriginalGHA) { + Remove-Item Env:GITHUB_ACTIONS -ErrorAction SilentlyContinue + } + else { + $env:GITHUB_ACTIONS = $script:OriginalGHA + } + if ($null -eq $script:OriginalTFBuild) { + Remove-Item Env:TF_BUILD -ErrorAction SilentlyContinue + } + else { + $env:TF_BUILD = $script:OriginalTFBuild + } + } + + BeforeEach { + # Reset environment + Remove-Item Env:GITHUB_ACTIONS -ErrorAction SilentlyContinue + Remove-Item Env:TF_BUILD -ErrorAction SilentlyContinue + } + + Context 'Repo root resolution' { + It 'Uses provided RepoRoot parameter' { + # Create minimal test repo structure + $testRepo = Join-Path $TestDrive 'test-repo' + New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null + Copy-Item -Path $script:ValidRegistryPath -Destination "$testRepo/.github/ai-artifacts-registry.json" + + # Mock Test-Path to simulate files exist + Mock Test-Path { return $true } -ParameterFilter { $Path -notlike '*results.json*' } + + # The script should not error when running with valid RepoRoot + { & $script:MainScriptPath -RepoRoot $testRepo -RegistryPath "$testRepo/.github/ai-artifacts-registry.json" -OutputPath "$testRepo/logs/results.json" 2>$null } | Should -Not -Throw + } + } + + Context 'Validation orchestration' { + It 'Reports error when registry file not found' { + $testRepo = Join-Path $TestDrive 'no-registry-repo' + New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null + + $output = & $script:MainScriptPath -RepoRoot $testRepo -RegistryPath "$testRepo/.github/nonexistent.json" -OutputPath "$testRepo/logs/results.json" 2>&1 + $output | Should -Match 'Registry file not found' + } + + It 'Stops validation early on structure failure' { + $testRepo = Join-Path $TestDrive 'invalid-structure-repo' + New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null + Copy-Item -Path $script:InvalidJsonPath -Destination "$testRepo/.github/ai-artifacts-registry.json" + + # Run directly with output suppression - exit code capture via $LASTEXITCODE + $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo' -OutputPath '$testRepo/logs/results.json'" 2>&1 + $LASTEXITCODE | Should -Be 1 + } + } + + Context 'CI environment handling' { + It 'Writes CI annotations when in GitHub Actions' { + $env:GITHUB_ACTIONS = 'true' + $testRepo = Join-Path $TestDrive 'gha-repo' + New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null + Copy-Item -Path $script:InvalidVersionPath -Destination "$testRepo/.github/ai-artifacts-registry.json" + + $output = & $script:MainScriptPath -RepoRoot $testRepo -OutputPath "$testRepo/logs/results.json" 2>&1 | Out-String + $output | Should -Match '::error' + } + + It 'Does not write CI annotations when not in CI' { + Remove-Item Env:GITHUB_ACTIONS -ErrorAction SilentlyContinue + Remove-Item Env:TF_BUILD -ErrorAction SilentlyContinue + $testRepo = Join-Path $TestDrive 'local-repo' + New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null + Copy-Item -Path $script:InvalidVersionPath -Destination "$testRepo/.github/ai-artifacts-registry.json" + + $output = & $script:MainScriptPath -RepoRoot $testRepo -OutputPath "$testRepo/logs/results.json" 2>&1 | Out-String + $output | Should -Not -Match '::error' + } + + It 'Writes CI warning annotations for orphan files in GitHub Actions' { + $env:GITHUB_ACTIONS = 'true' + $testRepo = Join-Path $TestDrive 'gha-warnings-repo' + New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/agents" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/prompts" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/instructions" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/skills/test-skill" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null + Copy-Item -Path $script:ValidRegistryPath -Destination "$testRepo/.github/ai-artifacts-registry.json" + + # Create the referenced artifact files + Set-Content -Path "$testRepo/.github/agents/test-agent.agent.md" -Value '# Test Agent' + Set-Content -Path "$testRepo/.github/agents/dependent-agent.agent.md" -Value '# Dependent Agent' + Set-Content -Path "$testRepo/.github/prompts/test-prompt.prompt.md" -Value '# Test Prompt' + Set-Content -Path "$testRepo/.github/instructions/test-instruction.instructions.md" -Value '# Test Instruction' + Set-Content -Path "$testRepo/.github/skills/test-skill/SKILL.md" -Value '# Test Skill' + + # Add orphan file to trigger warning + Set-Content -Path "$testRepo/.github/agents/orphan-unregistered.agent.md" -Value '# Orphan Agent' + + $output = & $script:MainScriptPath -RepoRoot $testRepo -OutputPath "$testRepo/logs/results.json" 2>&1 | Out-String + # Should have warning annotation for orphan file + $output | Should -Match '::warning' + } + + It 'Writes CI error annotation on exception in GitHub Actions' { + $env:GITHUB_ACTIONS = 'true' + $nonexistentRepo = '/completely/nonexistent/path/for/exception/test' + + $output = & $script:MainScriptPath -RepoRoot $nonexistentRepo 2>&1 | Out-String + # Should have error annotation for exception + $output | Should -Match '::error' + } + } + + Context 'Exit code handling' { + It 'Returns exit code 0 on success' { + $testRepo = Join-Path $TestDrive 'success-repo' + New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/agents" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/prompts" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/instructions" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/skills/test-skill" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null + Copy-Item -Path $script:ValidRegistryPath -Destination "$testRepo/.github/ai-artifacts-registry.json" + + # Create the referenced artifact files + Set-Content -Path "$testRepo/.github/agents/test-agent.agent.md" -Value '# Test Agent' + Set-Content -Path "$testRepo/.github/agents/dependent-agent.agent.md" -Value '# Dependent Agent' + Set-Content -Path "$testRepo/.github/prompts/test-prompt.prompt.md" -Value '# Test Prompt' + Set-Content -Path "$testRepo/.github/instructions/test-instruction.instructions.md" -Value '# Test Instruction' + Set-Content -Path "$testRepo/.github/skills/test-skill/SKILL.md" -Value '# Test Skill' + + # Run directly with output suppression + $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo' -OutputPath '$testRepo/logs/results.json'; exit `$LASTEXITCODE" 2>&1 + $LASTEXITCODE | Should -Be 0 + } + + It 'Returns exit code 1 when errors exist' { + $testRepo = Join-Path $TestDrive 'error-repo' + New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null + Copy-Item -Path $script:InvalidVersionPath -Destination "$testRepo/.github/ai-artifacts-registry.json" + + $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo' -OutputPath '$testRepo/logs/results.json'; exit `$LASTEXITCODE" 2>&1 + $LASTEXITCODE | Should -Be 1 + } + + It 'Returns exit code 1 with WarningsAsErrors and warnings' { + $testRepo = Join-Path $TestDrive 'warnings-as-errors-repo' + New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/agents" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/prompts" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/instructions" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/skills/test-skill" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null + Copy-Item -Path $script:ValidRegistryPath -Destination "$testRepo/.github/ai-artifacts-registry.json" + + # Create the referenced artifact files + Set-Content -Path "$testRepo/.github/agents/test-agent.agent.md" -Value '# Test Agent' + Set-Content -Path "$testRepo/.github/agents/dependent-agent.agent.md" -Value '# Dependent Agent' + Set-Content -Path "$testRepo/.github/prompts/test-prompt.prompt.md" -Value '# Test Prompt' + Set-Content -Path "$testRepo/.github/instructions/test-instruction.instructions.md" -Value '# Test Instruction' + Set-Content -Path "$testRepo/.github/skills/test-skill/SKILL.md" -Value '# Test Skill' + + # Add an orphan file to trigger a warning + Set-Content -Path "$testRepo/.github/agents/orphan-agent.agent.md" -Value '# Orphan' + + $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo' -OutputPath '$testRepo/logs/results.json' -WarningsAsErrors; exit `$LASTEXITCODE" 2>&1 + $LASTEXITCODE | Should -Be 1 + } + } + + Context 'Exception handling' { + It 'Exits with code 1 on exception' { + # Trigger exception by providing invalid path type + $testRepo = '/nonexistent/path/that/will/cause/error' + $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo'; exit `$LASTEXITCODE" 2>&1 + $LASTEXITCODE | Should -Be 1 + } + } +} + +#endregion + +#region Edge Cases Tests + +Describe 'Edge Cases' -Tag 'Unit' { + Context 'Empty registry sections' { + It 'Handles registry with no agents' { + $registry = @{ + agents = @{} + prompts = @{ 'p1' = @{} } + instructions = @{} + skills = @{} + } + Mock Test-Path { return $true } + $result = Test-ArtifactFileExistence -Registry $registry -RepoRoot $TestDrive + $result.Success | Should -BeTrue + } + + It 'Handles empty personas definitions' { + $registry = @{ + personas = @{ definitions = @{} } + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Test-PersonaReferences -Registry $registry + $result.Success | Should -BeTrue + } + } + + Context 'Complex dependency chains' { + It 'Handles deeply nested non-circular dependencies' { + $registry = @{ + agents = @{ + 'agent-1' = @{ requires = @{ agents = @('agent-2') } } + 'agent-2' = @{ requires = @{ agents = @('agent-3') } } + 'agent-3' = @{ requires = @{ agents = @('agent-4') } } + 'agent-4' = @{ requires = @{ agents = @('agent-5') } } + 'agent-5' = @{ requires = @{ agents = @() } } + } + } + $result = Find-CircularAgentDependencies -Registry $registry + $result.Count | Should -Be 0 + } + + It 'Handles diamond dependency pattern without cycles' { + $registry = @{ + agents = @{ + 'top' = @{ requires = @{ agents = @('left', 'right') } } + 'left' = @{ requires = @{ agents = @('bottom') } } + 'right' = @{ requires = @{ agents = @('bottom') } } + 'bottom' = @{ requires = @{ agents = @() } } + } + } + $result = Find-CircularAgentDependencies -Registry $registry + $result.Count | Should -Be 0 + } + } +} + +#endregion From bd091299eab8b8b3f1a73e5f5e729d1c63d95066 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 12:11:40 +0100 Subject: [PATCH 03/62] feat(scripts): add dependency pinning scan artifacts to .gitignore and improve RepoRoot resolution logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add dependency-pinning-artifacts/ to .gitignore - simplify RepoRoot path resolution in Validate-ArtifactRegistry.ps1 ๐Ÿ”’ - Generated by Copilot --- .gitignore | 3 + scripts/linting/Validate-ArtifactRegistry.ps1 | 10 +-- .../Validate-ArtifactRegistry.Tests.ps1 | 80 +++++++++++++++++++ 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 9b539995..4fd260af 100644 --- a/.gitignore +++ b/.gitignore @@ -442,6 +442,9 @@ checkov-junit.xml pr.md pr-reference.xml +# Dependency pinning scan artifacts +dependency-pinning-artifacts/ + # Copilot tracking .copilot-tracking/ diff --git a/scripts/linting/Validate-ArtifactRegistry.ps1 b/scripts/linting/Validate-ArtifactRegistry.ps1 index 76d1bcbd..cbf7d861 100644 --- a/scripts/linting/Validate-ArtifactRegistry.ps1 +++ b/scripts/linting/Validate-ArtifactRegistry.ps1 @@ -520,15 +520,9 @@ function Export-RegistryValidationResults { try { if ($MyInvocation.InvocationName -ne '.') { - # Resolve paths + # Resolve paths - script lives at scripts/linting/, so grandparent is repo root if (-not $RepoRoot) { - $RepoRoot = $PSScriptRoot - while ($RepoRoot -and -not (Test-Path (Join-Path $RepoRoot '.git'))) { - $RepoRoot = Split-Path -Parent $RepoRoot - } - if (-not $RepoRoot) { - throw "Could not find repository root" - } + $RepoRoot = (Resolve-Path "$PSScriptRoot/../..").Path } if (-not $RegistryPath) { diff --git a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 index dd3770da..2f27c680 100644 --- a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 +++ b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 @@ -699,6 +699,15 @@ Describe 'Main Execution Block' -Tag 'Integration' { # The script should not error when running with valid RepoRoot { & $script:MainScriptPath -RepoRoot $testRepo -RegistryPath "$testRepo/.github/ai-artifacts-registry.json" -OutputPath "$testRepo/logs/results.json" 2>$null } | Should -Not -Throw } + + It 'Derives RepoRoot from PSScriptRoot grandparent when not provided' { + # Run from the actual repo - without RepoRoot the script resolves $PSScriptRoot/../.. + $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -OutputPath '$TestDrive/results.json'; exit `$LASTEXITCODE" 2>&1 + # Script should resolve RepoRoot and produce output + Test-Path "$TestDrive/results.json" | Should -BeTrue + } + + } Context 'Validation orchestration' { @@ -809,6 +818,30 @@ Describe 'Main Execution Block' -Tag 'Integration' { $LASTEXITCODE | Should -Be 0 } + It 'Returns exit code 0 on success with default OutputPath' { + $testRepo = Join-Path $TestDrive 'success-default-output-repo' + New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/agents" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/prompts" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/instructions" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/skills/test-skill" -Force | Out-Null + Copy-Item -Path $script:ValidRegistryPath -Destination "$testRepo/.github/ai-artifacts-registry.json" + + # Create the referenced artifact files + Set-Content -Path "$testRepo/.github/agents/test-agent.agent.md" -Value '# Test Agent' + Set-Content -Path "$testRepo/.github/agents/dependent-agent.agent.md" -Value '# Dependent Agent' + Set-Content -Path "$testRepo/.github/prompts/test-prompt.prompt.md" -Value '# Test Prompt' + Set-Content -Path "$testRepo/.github/instructions/test-instruction.instructions.md" -Value '# Test Instruction' + Set-Content -Path "$testRepo/.github/skills/test-skill/SKILL.md" -Value '# Test Skill' + + # Run without OutputPath - should use default logs/registry-validation-results.json + $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo'; exit `$LASTEXITCODE" 2>&1 + $LASTEXITCODE | Should -Be 0 + + # Verify default output path was used + Test-Path "$testRepo/logs/registry-validation-results.json" | Should -BeTrue + } + It 'Returns exit code 1 when errors exist' { $testRepo = Join-Path $TestDrive 'error-repo' New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null @@ -843,6 +876,31 @@ Describe 'Main Execution Block' -Tag 'Integration' { $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo' -OutputPath '$testRepo/logs/results.json' -WarningsAsErrors; exit `$LASTEXITCODE" 2>&1 $LASTEXITCODE | Should -Be 1 } + + It 'Returns exit code 0 with warnings but without WarningsAsErrors flag' { + $testRepo = Join-Path $TestDrive 'warnings-no-error-repo' + New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/agents" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/prompts" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/instructions" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/.github/skills/test-skill" -Force | Out-Null + New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null + Copy-Item -Path $script:ValidRegistryPath -Destination "$testRepo/.github/ai-artifacts-registry.json" + + # Create the referenced artifact files + Set-Content -Path "$testRepo/.github/agents/test-agent.agent.md" -Value '# Test Agent' + Set-Content -Path "$testRepo/.github/agents/dependent-agent.agent.md" -Value '# Dependent Agent' + Set-Content -Path "$testRepo/.github/prompts/test-prompt.prompt.md" -Value '# Test Prompt' + Set-Content -Path "$testRepo/.github/instructions/test-instruction.instructions.md" -Value '# Test Instruction' + Set-Content -Path "$testRepo/.github/skills/test-skill/SKILL.md" -Value '# Test Skill' + + # Add an orphan file to trigger a warning + Set-Content -Path "$testRepo/.github/agents/orphan-agent.agent.md" -Value '# Orphan' + + # Without WarningsAsErrors, should still pass + $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo' -OutputPath '$testRepo/logs/results.json'; exit `$LASTEXITCODE" 2>&1 + $LASTEXITCODE | Should -Be 0 + } } Context 'Exception handling' { @@ -852,6 +910,28 @@ Describe 'Main Execution Block' -Tag 'Integration' { $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo'; exit `$LASTEXITCODE" 2>&1 $LASTEXITCODE | Should -Be 1 } + + It 'Writes CI error annotation when exception occurs in GitHub Actions' { + $env:GITHUB_ACTIONS = 'true' + # Use completely invalid path to trigger exception in catch block + $invalidRepo = '/this/path/does/not/exist/anywhere' + + $output = & $script:MainScriptPath -RepoRoot $invalidRepo 2>&1 | Out-String + + # Should have error annotation from the catch block + $output | Should -Match '::error.*Registry validation failed' + } + + It 'Writes CI error annotation when exception occurs in Azure DevOps' { + $env:TF_BUILD = 'True' + # Use completely invalid path to trigger exception in catch block + $invalidRepo = '/this/path/does/not/exist/anywhere' + + $output = & $script:MainScriptPath -RepoRoot $invalidRepo 2>&1 | Out-String + + # Should have error annotation from the catch block (Azure DevOps format) + $output | Should -Match '##vso\[task\.logissue.*error.*Registry validation failed' + } } } From b86044cef0496bf1f2fff42fe2f8757188472c0a Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 12:44:51 +0100 Subject: [PATCH 04/62] style(scripts): fix formatting of copyright comment in Validate-ArtifactRegistry.ps1 --- scripts/linting/Validate-ArtifactRegistry.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/linting/Validate-ArtifactRegistry.ps1 b/scripts/linting/Validate-ArtifactRegistry.ps1 index cbf7d861..8b934909 100644 --- a/scripts/linting/Validate-ArtifactRegistry.ps1 +++ b/scripts/linting/Validate-ArtifactRegistry.ps1 @@ -1,4 +1,4 @@ -# Copyright (c) Microsoft Corporation. +๏ปฟ# Copyright (c) Microsoft Corporation. # SPDX-License-Identifier: MIT # Validate-ArtifactRegistry.ps1 From cebe0f45a9e279661a5cd03202d03af9dde56689 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 13:18:27 +0100 Subject: [PATCH 05/62] refactor(schema): update agent dependencies description for clarity --- .github/ai-artifacts-registry.json | 24 +++++-------------- .../schemas/ai-artifacts-registry.schema.json | 2 +- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/.github/ai-artifacts-registry.json b/.github/ai-artifacts-registry.json index 224589a1..ab029699 100644 --- a/.github/ai-artifacts-registry.json +++ b/.github/ai-artifacts-registry.json @@ -204,9 +204,7 @@ "session" ], "requires": { - "agents": [ - "rpi-agent" - ], + "agents": [], "prompts": [ "checkpoint", "rpi" @@ -282,8 +280,7 @@ "task-researcher", "task-planner", "task-implementor", - "task-reviewer", - "memory" + "task-reviewer" ], "prompts": [ "task-research", @@ -325,9 +322,7 @@ "implementation" ], "requires": { - "agents": [ - "task-reviewer" - ], + "agents": [], "prompts": [], "instructions": [ "commit-message" @@ -345,9 +340,7 @@ "planning" ], "requires": { - "agents": [ - "task-implementor" - ], + "agents": [], "prompts": [], "instructions": [], "skills": [] @@ -363,9 +356,7 @@ "research" ], "requires": { - "agents": [ - "task-planner" - ], + "agents": [], "prompts": [], "instructions": [], "skills": [] @@ -381,10 +372,7 @@ "review" ], "requires": { - "agents": [ - "task-researcher", - "task-planner" - ], + "agents": [], "prompts": [], "instructions": [], "skills": [] diff --git a/scripts/linting/schemas/ai-artifacts-registry.schema.json b/scripts/linting/schemas/ai-artifacts-registry.schema.json index e5db7d54..7db44347 100644 --- a/scripts/linting/schemas/ai-artifacts-registry.schema.json +++ b/scripts/linting/schemas/ai-artifacts-registry.schema.json @@ -126,7 +126,7 @@ "items": { "type": "string" }, - "description": "Agent names this artifact depends on (e.g., handoff targets)" + "description": "Agent names this artifact dispatches at runtime via runSubagent. Handoff targets (frontmatter handoffs field) are resolved dynamically during packaging and excluded from this array to avoid circular dependencies." }, "prompts": { "type": "array", From aabe010fa7056c9c85e1cfcaa40b5e53114a5762 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:29:08 +0100 Subject: [PATCH 06/62] refactor(schemas): migrate maturity metadata to registry, remove from frontmatter (#440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #438 - [x] Explore and understand all files requiring changes - [x] **Phase 1: Schema Updates** โ€” Remove `maturity` property from 5 schema files - [x] **Phase 2: Artifact Frontmatter Removal** โ€” Remove `maturity` line from all artifact files - [x] **Phase 3: Documentation Updates** โ€” Update docs to reference registry - [x] Step 3.1: Rewrite `ai-artifacts-common.md` maturity section - [x] Step 3.2: Remove maturity from `custom-agents.md` - [x] Step 3.3: Remove maturity from `instructions.md` - [x] Step 3.4: Remove maturity from `prompts.md` - [x] Step 3.5: Remove maturity from `skills.md` - [x] Step 3.6: Update `release-process.md` maturity section - [x] Step 3.7: Update `prompt-builder.instructions.md` โ€” add registry maturity guidance - [x] Step 3.8: Update `pull-request.prompt.md` maturity detection - [x] Run code review and security checks โ€” โœ… No issues **Security Summary**: No security vulnerabilities. All changes are documentation and JSON schema modifications only. --- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: katriendg <838216+katriendg@users.noreply.github.com> --- .github/agents/ado-prd-to-wit.agent.md | 1 - .github/agents/adr-creation.agent.md | 1 - .github/agents/arch-diagram-builder.agent.md | 1 - .github/agents/brd-builder.agent.md | 1 - .github/agents/doc-ops.agent.md | 1 - .github/agents/gen-data-spec.agent.md | 1 - .github/agents/gen-jupyter-notebook.agent.md | 1 - .../agents/gen-streamlit-dashboard.agent.md | 1 - .github/agents/github-issue-manager.agent.md | 1 - .github/agents/hve-core-installer.agent.md | 1 - .github/agents/memory.agent.md | 1 - .github/agents/pr-review.agent.md | 1 - .github/agents/prd-builder.agent.md | 1 - .github/agents/prompt-builder.agent.md | 1 - .github/agents/rpi-agent.agent.md | 1 - .github/agents/security-plan-creator.agent.md | 1 - .github/agents/task-implementor.agent.md | 1 - .github/agents/task-planner.agent.md | 1 - .github/agents/task-researcher.agent.md | 1 - .github/agents/task-reviewer.agent.md | 1 - .../agents/test-streamlit-dashboard.agent.md | 1 - .../ado-create-pull-request.instructions.md | 1 - .../ado-get-build-info.instructions.md | 1 - .../ado-update-wit-items.instructions.md | 1 - .../ado-wit-discovery.instructions.md | 1 - .../ado-wit-planning.instructions.md | 1 - .../instructions/bash/bash.instructions.md | 1 - .../instructions/bicep/bicep.instructions.md | 1 - .../commit-message.instructions.md | 1 - .../csharp/csharp-tests.instructions.md | 1 - .../csharp/csharp.instructions.md | 1 - .../instructions/git-merge.instructions.md | 1 - .../hve-core-location.instructions.md | 1 - .github/instructions/markdown.instructions.md | 1 - .../prompt-builder.instructions.md | 7 ++----- .../python-script.instructions.md | 1 - .../terraform/terraform.instructions.md | 1 - .../instructions/uv-projects.instructions.md | 1 - .../writing-style.instructions.md | 1 - .../prompts/ado-create-pull-request.prompt.md | 1 - .github/prompts/ado-get-build-info.prompt.md | 1 - .../prompts/ado-get-my-work-items.prompt.md | 1 - ...-my-work-items-for-task-planning.prompt.md | 1 - .../prompts/ado-update-wit-items.prompt.md | 1 - .github/prompts/checkpoint.prompt.md | 1 - .github/prompts/doc-ops-update.prompt.md | 1 - .github/prompts/git-commit-message.prompt.md | 1 - .github/prompts/git-commit.prompt.md | 1 - .github/prompts/git-merge.prompt.md | 1 - .github/prompts/git-setup.prompt.md | 1 - .github/prompts/github-add-issue.prompt.md | 1 - .github/prompts/prompt-analyze.prompt.md | 1 - .github/prompts/prompt-build.prompt.md | 1 - .github/prompts/prompt-refactor.prompt.md | 1 - .github/prompts/pull-request.prompt.md | 9 ++++----- .github/prompts/risk-register.prompt.md | 1 - .github/prompts/rpi.prompt.md | 1 - .github/prompts/task-implement.prompt.md | 1 - .github/prompts/task-plan.prompt.md | 1 - .github/prompts/task-research.prompt.md | 1 - .github/prompts/task-review.prompt.md | 1 - .github/skills/video-to-gif/SKILL.md | 1 - docs/contributing/ai-artifacts-common.md | 20 ++++++++++--------- docs/contributing/custom-agents.md | 11 ---------- docs/contributing/instructions.md | 12 ----------- docs/contributing/prompts.md | 14 +------------ docs/contributing/release-process.md | 12 +++++------ docs/contributing/skills.md | 7 ------- .../schemas/agent-frontmatter.schema.json | 6 ------ .../schemas/chatmode-frontmatter.schema.json | 6 ------ .../instruction-frontmatter.schema.json | 6 ------ .../schemas/prompt-frontmatter.schema.json | 6 ------ .../schemas/skill-frontmatter.schema.json | 7 +------ 73 files changed, 25 insertions(+), 158 deletions(-) diff --git a/.github/agents/ado-prd-to-wit.agent.md b/.github/agents/ado-prd-to-wit.agent.md index 969d4d0e..a9f4a146 100644 --- a/.github/agents/ado-prd-to-wit.agent.md +++ b/.github/agents/ado-prd-to-wit.agent.md @@ -1,6 +1,5 @@ --- description: 'Product Manager expert for analyzing PRDs and planning Azure DevOps work item hierarchies' -maturity: stable tools: ['execute/getTerminalOutput', 'execute/runInTerminal', 'read/problems', 'read/readFile', 'read/terminalSelection', 'read/terminalLastCommand', 'edit/createDirectory', 'edit/createFile', 'edit/editFiles', 'search', 'web', 'agent', 'ado/search_workitem', 'ado/wit_get_work_item', 'ado/wit_get_work_items_for_iteration', 'ado/wit_list_backlog_work_items', 'ado/wit_list_backlogs', 'ado/wit_list_work_item_comments', 'ado/work_list_team_iterations', 'microsoft-docs/*'] --- diff --git a/.github/agents/adr-creation.agent.md b/.github/agents/adr-creation.agent.md index fa981983..9d3603e7 100644 --- a/.github/agents/adr-creation.agent.md +++ b/.github/agents/adr-creation.agent.md @@ -1,6 +1,5 @@ --- description: 'Interactive AI coaching for collaborative architectural decision record creation with guided discovery, research integration, and progressive documentation building - Brought to you by microsoft/edge-ai' -maturity: stable --- # ADR Creation Coach diff --git a/.github/agents/arch-diagram-builder.agent.md b/.github/agents/arch-diagram-builder.agent.md index 7e0cf07a..1e14ec13 100644 --- a/.github/agents/arch-diagram-builder.agent.md +++ b/.github/agents/arch-diagram-builder.agent.md @@ -1,6 +1,5 @@ --- description: Architecture diagram builder agent that builds high quality ASCII-art diagrams - Brought to you by microsoft/hve-core -maturity: stable --- # Architecture Diagram Builder Agent diff --git a/.github/agents/brd-builder.agent.md b/.github/agents/brd-builder.agent.md index dbb51f1c..cc518cdb 100644 --- a/.github/agents/brd-builder.agent.md +++ b/.github/agents/brd-builder.agent.md @@ -1,6 +1,5 @@ --- description: "Business Requirements Document builder with guided Q&A and reference integration" -maturity: stable --- # BRD Builder Instructions diff --git a/.github/agents/doc-ops.agent.md b/.github/agents/doc-ops.agent.md index d9242348..1a31c5dc 100644 --- a/.github/agents/doc-ops.agent.md +++ b/.github/agents/doc-ops.agent.md @@ -1,6 +1,5 @@ --- description: 'Autonomous documentation operations agent for pattern compliance, accuracy verification, and gap detection - Brought to you by microsoft/hve-core' -maturity: stable --- # Documentation Operations Agent diff --git a/.github/agents/gen-data-spec.agent.md b/.github/agents/gen-data-spec.agent.md index e655ca4c..72684ef8 100644 --- a/.github/agents/gen-data-spec.agent.md +++ b/.github/agents/gen-data-spec.agent.md @@ -1,6 +1,5 @@ --- description: "Generate comprehensive data dictionaries, machine-readable data profiles, and objective summaries for downstream analysis (EDA notebooks, dashboards) through guided discovery" -maturity: stable tools: ['runCommands', 'edit/createFile', 'edit/createDirectory', 'edit/editFiles', 'search', 'think', 'todos'] --- diff --git a/.github/agents/gen-jupyter-notebook.agent.md b/.github/agents/gen-jupyter-notebook.agent.md index 3f02781c..d8126026 100644 --- a/.github/agents/gen-jupyter-notebook.agent.md +++ b/.github/agents/gen-jupyter-notebook.agent.md @@ -1,6 +1,5 @@ --- description: 'Create structured exploratory data analysis Jupyter notebooks from available data sources and generated data dictionaries' -maturity: stable --- # Jupyter Notebook Generator diff --git a/.github/agents/gen-streamlit-dashboard.agent.md b/.github/agents/gen-streamlit-dashboard.agent.md index 9a10aae7..f7c33f14 100644 --- a/.github/agents/gen-streamlit-dashboard.agent.md +++ b/.github/agents/gen-streamlit-dashboard.agent.md @@ -1,6 +1,5 @@ --- description: 'Develop a multi-page Streamlit dashboard' -maturity: stable --- # Streamlit Dashboard Generator diff --git a/.github/agents/github-issue-manager.agent.md b/.github/agents/github-issue-manager.agent.md index dd3e52dd..f530c152 100644 --- a/.github/agents/github-issue-manager.agent.md +++ b/.github/agents/github-issue-manager.agent.md @@ -1,6 +1,5 @@ --- description: 'Interactive GitHub issue management with conversational workflows for filing, navigating, and searching issues' -maturity: stable tools: ['execute/getTerminalOutput', 'execute/runInTerminal', 'read', 'edit/createDirectory', 'edit/createFile', 'edit/editFiles', 'search', 'web', 'agent', 'github/*'] --- diff --git a/.github/agents/hve-core-installer.agent.md b/.github/agents/hve-core-installer.agent.md index de40275f..3baf7e6d 100644 --- a/.github/agents/hve-core-installer.agent.md +++ b/.github/agents/hve-core-installer.agent.md @@ -1,6 +1,5 @@ --- description: 'Decision-driven installer for HVE-Core with 6 installation methods for local, devcontainer, and Codespaces environments - Brought to you by microsoft/hve-core' -maturity: stable tools: ['vscode/newWorkspace', 'vscode/runCommand', 'execute/runInTerminal', 'read', 'edit/createDirectory', 'edit/createFile', 'edit/editFiles', 'search', 'web', 'agent', 'todo'] --- # HVE-Core Installer Agent diff --git a/.github/agents/memory.agent.md b/.github/agents/memory.agent.md index aefad2ef..693fec20 100644 --- a/.github/agents/memory.agent.md +++ b/.github/agents/memory.agent.md @@ -1,6 +1,5 @@ --- description: "Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core" -maturity: stable handoffs: - label: "๐Ÿ—‘๏ธ Clear" agent: rpi-agent diff --git a/.github/agents/pr-review.agent.md b/.github/agents/pr-review.agent.md index 81d49735..5100f34a 100644 --- a/.github/agents/pr-review.agent.md +++ b/.github/agents/pr-review.agent.md @@ -1,6 +1,5 @@ --- description: 'Comprehensive Pull Request review assistant ensuring code quality, security, and convention compliance - Brought to you by microsoft/hve-core' -maturity: stable --- # PR Review Assistant diff --git a/.github/agents/prd-builder.agent.md b/.github/agents/prd-builder.agent.md index 01669981..7b9112b1 100644 --- a/.github/agents/prd-builder.agent.md +++ b/.github/agents/prd-builder.agent.md @@ -1,6 +1,5 @@ --- description: "Product Requirements Document builder with guided Q&A and reference integration" -maturity: stable --- # PRD Builder Instructions diff --git a/.github/agents/prompt-builder.agent.md b/.github/agents/prompt-builder.agent.md index 83b3c044..7b3b320e 100644 --- a/.github/agents/prompt-builder.agent.md +++ b/.github/agents/prompt-builder.agent.md @@ -1,6 +1,5 @@ --- description: 'Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core' -maturity: stable handoffs: - label: "๐Ÿ’ก Update/Create" agent: prompt-builder diff --git a/.github/agents/rpi-agent.agent.md b/.github/agents/rpi-agent.agent.md index b4fa1aa5..5d9ca2b6 100644 --- a/.github/agents/rpi-agent.agent.md +++ b/.github/agents/rpi-agent.agent.md @@ -1,6 +1,5 @@ --- description: 'Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core' -maturity: stable argument-hint: 'Autonomous RPI agent. Requires runSubagent tool.' handoffs: - label: "1๏ธโƒฃ" diff --git a/.github/agents/security-plan-creator.agent.md b/.github/agents/security-plan-creator.agent.md index ca6d6d6a..703206c4 100644 --- a/.github/agents/security-plan-creator.agent.md +++ b/.github/agents/security-plan-creator.agent.md @@ -1,6 +1,5 @@ --- description: "Expert security architect for creating comprehensive cloud security plans - Brought to you by microsoft/hve-core" -maturity: stable --- # Security Plan Creation Expert diff --git a/.github/agents/task-implementor.agent.md b/.github/agents/task-implementor.agent.md index dc85d724..02d6b7cc 100644 --- a/.github/agents/task-implementor.agent.md +++ b/.github/agents/task-implementor.agent.md @@ -1,6 +1,5 @@ --- description: 'Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records' -maturity: stable handoffs: - label: "โœ… Review" agent: task-reviewer diff --git a/.github/agents/task-planner.agent.md b/.github/agents/task-planner.agent.md index 1ef7ffff..fb72fc46 100644 --- a/.github/agents/task-planner.agent.md +++ b/.github/agents/task-planner.agent.md @@ -1,6 +1,5 @@ --- description: 'Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core' -maturity: stable handoffs: - label: "โšก Implement" agent: task-implementor diff --git a/.github/agents/task-researcher.agent.md b/.github/agents/task-researcher.agent.md index 2eb2aeb9..5abcebb5 100644 --- a/.github/agents/task-researcher.agent.md +++ b/.github/agents/task-researcher.agent.md @@ -1,6 +1,5 @@ --- description: 'Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core' -maturity: stable handoffs: - label: "๐Ÿ“‹ Create Plan" agent: task-planner diff --git a/.github/agents/task-reviewer.agent.md b/.github/agents/task-reviewer.agent.md index 09e6dafb..5c53f1fb 100644 --- a/.github/agents/task-reviewer.agent.md +++ b/.github/agents/task-reviewer.agent.md @@ -1,6 +1,5 @@ --- description: 'Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core' -maturity: stable handoffs: - label: "๐Ÿ”ฌ Research More" agent: task-researcher diff --git a/.github/agents/test-streamlit-dashboard.agent.md b/.github/agents/test-streamlit-dashboard.agent.md index 83a746ae..09216d6c 100644 --- a/.github/agents/test-streamlit-dashboard.agent.md +++ b/.github/agents/test-streamlit-dashboard.agent.md @@ -1,6 +1,5 @@ --- description: 'Automated testing for Streamlit dashboards using Playwright with issue tracking and reporting' -maturity: stable --- # Streamlit Dashboard Testing diff --git a/.github/instructions/ado-create-pull-request.instructions.md b/.github/instructions/ado-create-pull-request.instructions.md index 88ccc892..33e8b27e 100644 --- a/.github/instructions/ado-create-pull-request.instructions.md +++ b/.github/instructions/ado-create-pull-request.instructions.md @@ -1,7 +1,6 @@ --- description: "Required protocol for creating Azure DevOps pull requests with work item discovery, reviewer identification, and automated linking." applyTo: '**/.copilot-tracking/pr/new/**' -maturity: stable --- # Azure DevOps Pull Request Creation diff --git a/.github/instructions/ado-get-build-info.instructions.md b/.github/instructions/ado-get-build-info.instructions.md index 3187211e..8782bd49 100644 --- a/.github/instructions/ado-get-build-info.instructions.md +++ b/.github/instructions/ado-get-build-info.instructions.md @@ -1,7 +1,6 @@ --- description: 'Required instructions for anything related to Azure Devops or ado build information including status, logs, or details from provided pullrequest (PR), build Id, or branch name.' applyTo: '**/.copilot-tracking/pr/*-build-*.md' -maturity: stable --- # Azure DevOps Build Info Instructions diff --git a/.github/instructions/ado-update-wit-items.instructions.md b/.github/instructions/ado-update-wit-items.instructions.md index 1b1d9e0b..6a03985e 100644 --- a/.github/instructions/ado-update-wit-items.instructions.md +++ b/.github/instructions/ado-update-wit-items.instructions.md @@ -1,7 +1,6 @@ --- description: 'Work item creation and update protocol using MCP ADO tools with handoff tracking' applyTo: '**/.copilot-tracking/workitems/**/handoff-logs.md' -maturity: stable --- # Azure DevOps Work Item Update Instructions diff --git a/.github/instructions/ado-wit-discovery.instructions.md b/.github/instructions/ado-wit-discovery.instructions.md index 73f3cd43..992bb55a 100644 --- a/.github/instructions/ado-wit-discovery.instructions.md +++ b/.github/instructions/ado-wit-discovery.instructions.md @@ -1,7 +1,6 @@ --- description: 'Protocol for discovering Azure DevOps work items via user assignment or artifact analysis with planning file output' applyTo: '**/.copilot-tracking/workitems/discovery/**' -maturity: stable --- # Azure DevOps Work Item Discovery diff --git a/.github/instructions/ado-wit-planning.instructions.md b/.github/instructions/ado-wit-planning.instructions.md index 320bac92..78753101 100644 --- a/.github/instructions/ado-wit-planning.instructions.md +++ b/.github/instructions/ado-wit-planning.instructions.md @@ -2,7 +2,6 @@ name: 'ADO Work Item Planning' description: 'Reference specification for Azure DevOps work item planning files, templates, field definitions, and search protocols' applyTo: '**/.copilot-tracking/workitems/**' -maturity: stable --- # Azure DevOps Work Items Planning File Instructions diff --git a/.github/instructions/bash/bash.instructions.md b/.github/instructions/bash/bash.instructions.md index 35872caa..7c617391 100644 --- a/.github/instructions/bash/bash.instructions.md +++ b/.github/instructions/bash/bash.instructions.md @@ -1,7 +1,6 @@ --- applyTo: '**/*.sh' description: 'Instructions for bash script implementation - Brought to you by microsoft/edge-ai' -maturity: stable --- # Bash Script Instructions diff --git a/.github/instructions/bicep/bicep.instructions.md b/.github/instructions/bicep/bicep.instructions.md index 0608c800..95295e5a 100644 --- a/.github/instructions/bicep/bicep.instructions.md +++ b/.github/instructions/bicep/bicep.instructions.md @@ -1,7 +1,6 @@ --- applyTo: '**/bicep/**' description: 'Instructions for Bicep infrastructure as code implementation - Brought to you by microsoft/hve-core' -maturity: stable --- # Bicep Instructions diff --git a/.github/instructions/commit-message.instructions.md b/.github/instructions/commit-message.instructions.md index 1cef1b20..4515424c 100644 --- a/.github/instructions/commit-message.instructions.md +++ b/.github/instructions/commit-message.instructions.md @@ -1,6 +1,5 @@ --- description: 'Required instructions for creating all commit messages - Brought to you by microsoft/hve-core' -maturity: stable --- # Commit Message Guidelines diff --git a/.github/instructions/csharp/csharp-tests.instructions.md b/.github/instructions/csharp/csharp-tests.instructions.md index faf9528f..1a85f09b 100644 --- a/.github/instructions/csharp/csharp-tests.instructions.md +++ b/.github/instructions/csharp/csharp-tests.instructions.md @@ -1,7 +1,6 @@ --- applyTo: '**/*.cs' description: 'Required instructions for C# (CSharp) test code research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core' -maturity: stable --- # C# Test Instructions diff --git a/.github/instructions/csharp/csharp.instructions.md b/.github/instructions/csharp/csharp.instructions.md index 179362d2..2e3f0e45 100644 --- a/.github/instructions/csharp/csharp.instructions.md +++ b/.github/instructions/csharp/csharp.instructions.md @@ -1,7 +1,6 @@ --- applyTo: '**/*.cs' description: 'Required instructions for C# (CSharp) research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core' -maturity: stable --- # C# Instructions diff --git a/.github/instructions/git-merge.instructions.md b/.github/instructions/git-merge.instructions.md index 6e28b5c8..0e44df2e 100644 --- a/.github/instructions/git-merge.instructions.md +++ b/.github/instructions/git-merge.instructions.md @@ -1,6 +1,5 @@ --- description: "Required protocol for Git merge, rebase, and rebase --onto workflows with conflict handling and stop controls." -maturity: stable --- # Git Merge & Rebase Instructions diff --git a/.github/instructions/hve-core-location.instructions.md b/.github/instructions/hve-core-location.instructions.md index b80234d1..883acb76 100644 --- a/.github/instructions/hve-core-location.instructions.md +++ b/.github/instructions/hve-core-location.instructions.md @@ -1,7 +1,6 @@ --- description: "Important: hve-core is the repository containing this instruction file; Guidance: if a referenced prompt, instructions, agent, or script is missing in the current directory, fall back to this hve-core location by walking up this file's directory tree." applyTo: "**" -maturity: stable --- # HVE Core Location Guidance diff --git a/.github/instructions/markdown.instructions.md b/.github/instructions/markdown.instructions.md index 8a5f483a..bcfca513 100644 --- a/.github/instructions/markdown.instructions.md +++ b/.github/instructions/markdown.instructions.md @@ -1,7 +1,6 @@ --- description: "Required instructions for creating or editing any Markdown (.md) files" applyTo: '**/*.md' -maturity: stable --- # Markdown Instructions diff --git a/.github/instructions/prompt-builder.instructions.md b/.github/instructions/prompt-builder.instructions.md index 2a26558f..5940c3dd 100644 --- a/.github/instructions/prompt-builder.instructions.md +++ b/.github/instructions/prompt-builder.instructions.md @@ -1,7 +1,6 @@ --- description: 'Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core' applyTo: '**/*.prompt.md, **/*.agent.md, **/*.instructions.md, **/SKILL.md' -maturity: stable --- # Prompt Builder Instructions @@ -205,7 +204,6 @@ Validation guidelines: * Include `name` frontmatter matching the skill directory name (required). * Include `description` frontmatter (required). -* Include `maturity` frontmatter (required). * Provide parallel script implementations for bash and PowerShell when targeting cross-platform use. * Document prerequisites for each supported platform. * Keep *SKILL.md* under 500 lines; move detailed reference material to `references/`. @@ -215,14 +213,13 @@ Validation guidelines: This section defines frontmatter field requirements for prompt engineering artifacts. +Maturity is tracked in `.github/ai-artifacts-registry.json`, not in frontmatter. Do not include a `maturity` field in artifact frontmatter. Set the maturity value in the artifact's registry entry instead. + ### Required Fields All prompt engineering artifacts include these frontmatter fields: * `description:` - Brief description of the artifact's purpose. -* `maturity:` - Lifecycle stage: `experimental`, `preview`, `stable`, or `deprecated`. - -Note: VS Code shows a validation warning for the `maturity:` field as it's not in VS Code's schema. This is expected; the field is required by the HVE-Core codebase for artifact lifecycle tracking. Ignore VS Code validation warnings for the `maturity:` attribute. ### Optional Fields diff --git a/.github/instructions/python-script.instructions.md b/.github/instructions/python-script.instructions.md index 04673f91..a54032f0 100644 --- a/.github/instructions/python-script.instructions.md +++ b/.github/instructions/python-script.instructions.md @@ -1,7 +1,6 @@ --- applyTo: '**/*.py' description: 'Instructions for Python scripting implementation - Brought to you by microsoft/hve-core' -maturity: stable --- # Python Script Instructions diff --git a/.github/instructions/terraform/terraform.instructions.md b/.github/instructions/terraform/terraform.instructions.md index a65743d1..6ad12de8 100644 --- a/.github/instructions/terraform/terraform.instructions.md +++ b/.github/instructions/terraform/terraform.instructions.md @@ -1,7 +1,6 @@ --- applyTo: '**/*.tf, **/*.tfvars, **/terraform/**' description: 'Instructions for Terraform infrastructure as code implementation - Brought to you by microsoft/hve-core' -maturity: stable --- # Terraform Instructions diff --git a/.github/instructions/uv-projects.instructions.md b/.github/instructions/uv-projects.instructions.md index b5cdce45..fec87908 100644 --- a/.github/instructions/uv-projects.instructions.md +++ b/.github/instructions/uv-projects.instructions.md @@ -1,7 +1,6 @@ --- description: 'Create and manage Python virtual environments using uv commands' applyTo: '**/*.py, **/*.ipynb' -maturity: stable --- # UV Environment Management diff --git a/.github/instructions/writing-style.instructions.md b/.github/instructions/writing-style.instructions.md index 51f51c1d..6fe69616 100644 --- a/.github/instructions/writing-style.instructions.md +++ b/.github/instructions/writing-style.instructions.md @@ -1,7 +1,6 @@ --- description: "Required writing style conventions for voice, tone, and language in all markdown content" applyTo: '**/*.md' -maturity: stable --- # Writing Style Instructions diff --git a/.github/prompts/ado-create-pull-request.prompt.md b/.github/prompts/ado-create-pull-request.prompt.md index cc840aa4..04cbe0ee 100644 --- a/.github/prompts/ado-create-pull-request.prompt.md +++ b/.github/prompts/ado-create-pull-request.prompt.md @@ -1,6 +1,5 @@ --- description: "Generate pull request description, discover related work items, identify reviewers, and create Azure DevOps pull request with all linkages." -maturity: stable --- # Create Azure DevOps Pull Request with Work Item & Reviewer Discovery diff --git a/.github/prompts/ado-get-build-info.prompt.md b/.github/prompts/ado-get-build-info.prompt.md index 5956fc80..4cbeead0 100644 --- a/.github/prompts/ado-get-build-info.prompt.md +++ b/.github/prompts/ado-get-build-info.prompt.md @@ -1,6 +1,5 @@ --- description: "Retrieve Azure DevOps build information for a Pull Request or specific Build Number." -maturity: stable --- # ADO Build Info & Log Extraction (Targeted or Latest PR Build) diff --git a/.github/prompts/ado-get-my-work-items.prompt.md b/.github/prompts/ado-get-my-work-items.prompt.md index 23179c60..b152b797 100644 --- a/.github/prompts/ado-get-my-work-items.prompt.md +++ b/.github/prompts/ado-get-my-work-items.prompt.md @@ -1,6 +1,5 @@ --- description: "Retrieve user's current Azure DevOps work items and organize them into planning file definitions" -maturity: stable --- # Get My Work Items and Create Planning Files diff --git a/.github/prompts/ado-process-my-work-items-for-task-planning.prompt.md b/.github/prompts/ado-process-my-work-items-for-task-planning.prompt.md index 34908a4e..f3fddc26 100644 --- a/.github/prompts/ado-process-my-work-items-for-task-planning.prompt.md +++ b/.github/prompts/ado-process-my-work-items-for-task-planning.prompt.md @@ -1,6 +1,5 @@ --- description: "Process retrieved work items for task planning and generate task-planning-logs.md handoff file" -maturity: stable --- # Process My Work Items for Task Planning diff --git a/.github/prompts/ado-update-wit-items.prompt.md b/.github/prompts/ado-update-wit-items.prompt.md index 46ecef2b..e5e216ed 100644 --- a/.github/prompts/ado-update-wit-items.prompt.md +++ b/.github/prompts/ado-update-wit-items.prompt.md @@ -1,6 +1,5 @@ --- description: "Prompt to update work items based on planning files" -maturity: stable --- # Update Work Items diff --git a/.github/prompts/checkpoint.prompt.md b/.github/prompts/checkpoint.prompt.md index 4e32e84a..6c8252af 100644 --- a/.github/prompts/checkpoint.prompt.md +++ b/.github/prompts/checkpoint.prompt.md @@ -1,7 +1,6 @@ --- description: "Save or restore conversation context using memory files - Brought to you by microsoft/hve-core" agent: 'memory' -maturity: stable argument-hint: "[mode={save|continue|incremental}] [description=...]" --- diff --git a/.github/prompts/doc-ops-update.prompt.md b/.github/prompts/doc-ops-update.prompt.md index 66c9ecfd..dd7a4d19 100644 --- a/.github/prompts/doc-ops-update.prompt.md +++ b/.github/prompts/doc-ops-update.prompt.md @@ -2,7 +2,6 @@ description: 'Invoke doc-ops agent for documentation quality assurance and updates' agent: 'doc-ops' argument-hint: '[scope=all|docs|root|scripts] [validate-only={true|false}]' -maturity: stable --- # Documentation Update diff --git a/.github/prompts/git-commit-message.prompt.md b/.github/prompts/git-commit-message.prompt.md index d2a93af2..cf3e7627 100644 --- a/.github/prompts/git-commit-message.prompt.md +++ b/.github/prompts/git-commit-message.prompt.md @@ -1,7 +1,6 @@ --- agent: 'agent' description: 'Generates a commit message following the commit-message.instructions.md rules based on all changes in the branch' -maturity: stable --- # Generate Commit Message diff --git a/.github/prompts/git-commit.prompt.md b/.github/prompts/git-commit.prompt.md index 3b21dfc6..f396492d 100644 --- a/.github/prompts/git-commit.prompt.md +++ b/.github/prompts/git-commit.prompt.md @@ -1,7 +1,6 @@ --- agent: 'agent' description: 'Stages all changes, generates a conventional commit message, shows it to the user, and commits using only git add/commit' -maturity: stable --- # Stage, Generate, and Commit diff --git a/.github/prompts/git-merge.prompt.md b/.github/prompts/git-merge.prompt.md index 0fc51e70..7f0293b5 100644 --- a/.github/prompts/git-merge.prompt.md +++ b/.github/prompts/git-merge.prompt.md @@ -1,7 +1,6 @@ --- agent: 'agent' description: 'Coordinate Git merge, rebase, and rebase --onto workflows with consistent conflict handling.' -maturity: stable --- # Git Merge & Rebase Orchestrator diff --git a/.github/prompts/git-setup.prompt.md b/.github/prompts/git-setup.prompt.md index 8f7a49cd..9e8b1233 100644 --- a/.github/prompts/git-setup.prompt.md +++ b/.github/prompts/git-setup.prompt.md @@ -1,7 +1,6 @@ --- agent: 'agent' description: 'Interactive, verification-first Git configuration assistant (non-destructive)' -maturity: stable --- # Git Environment Setup (Verification-First) diff --git a/.github/prompts/github-add-issue.prompt.md b/.github/prompts/github-add-issue.prompt.md index c8c95090..0ef83e00 100644 --- a/.github/prompts/github-add-issue.prompt.md +++ b/.github/prompts/github-add-issue.prompt.md @@ -1,7 +1,6 @@ --- agent: "agent" description: "Add a GitHub issue to the backlog using discovered issue templates from .github/ISSUE_TEMPLATE/" -maturity: stable --- # Add GitHub Issue to Backlog diff --git a/.github/prompts/prompt-analyze.prompt.md b/.github/prompts/prompt-analyze.prompt.md index d5a7db6c..0fbde369 100644 --- a/.github/prompts/prompt-analyze.prompt.md +++ b/.github/prompts/prompt-analyze.prompt.md @@ -1,7 +1,6 @@ --- description: "Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core" argument-hint: "file=..." -maturity: stable --- # Prompt Analyze diff --git a/.github/prompts/prompt-build.prompt.md b/.github/prompts/prompt-build.prompt.md index ad7d38e1..be396b9b 100644 --- a/.github/prompts/prompt-build.prompt.md +++ b/.github/prompts/prompt-build.prompt.md @@ -2,7 +2,6 @@ description: "Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core" agent: 'prompt-builder' argument-hint: "file=... [requirements=...]" -maturity: stable --- # Prompt Build diff --git a/.github/prompts/prompt-refactor.prompt.md b/.github/prompts/prompt-refactor.prompt.md index e9eeee46..b23b9ece 100644 --- a/.github/prompts/prompt-refactor.prompt.md +++ b/.github/prompts/prompt-refactor.prompt.md @@ -2,7 +2,6 @@ description: "Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core" argument-hint: "file=..." agent: 'prompt-builder' -maturity: stable --- # Prompt Refactor diff --git a/.github/prompts/pull-request.prompt.md b/.github/prompts/pull-request.prompt.md index 81b98f82..4b1c7904 100644 --- a/.github/prompts/pull-request.prompt.md +++ b/.github/prompts/pull-request.prompt.md @@ -1,7 +1,6 @@ --- description: 'Provides prompt instructions for pull request (PR) generation - Brought to you by microsoft/edge-ai' agent: agent -maturity: stable --- # Pull Request (PR) Generation Instructions @@ -160,12 +159,12 @@ Deduplicate issue numbers and preserve the action prefix from the first occurren #### GHCP Maturity Detection -After detecting GHCP files from Change Type Detection, analyze frontmatter for maturity levels: +After detecting GHCP files from Change Type Detection, look up maturity levels from the AI Artifacts Registry: 1. For each file matching `.instructions.md`, `.prompt.md`, or `.agent.md` patterns: - * Extract file content from `` section (look for `+++ b/...` paths) - * Parse YAML frontmatter between `---` delimiters in the added content - * Read `maturity` field value (default: `stable` if not present) + * Derive the artifact key from the file path by removing the extension pattern (e.g., `pull-request.prompt.md` โ†’ `pull-request`, `bash/bash.instructions.md` โ†’ `bash/bash`) + * Look up the artifact's `maturity` value in `.github/ai-artifacts-registry.json` + * Default to `stable` if the artifact is not found in the registry 2. Categorize files by maturity: diff --git a/.github/prompts/risk-register.prompt.md b/.github/prompts/risk-register.prompt.md index efe70e4c..070edd0c 100644 --- a/.github/prompts/risk-register.prompt.md +++ b/.github/prompts/risk-register.prompt.md @@ -2,7 +2,6 @@ description: "Creates a concise and well-structured qualitative risk register using a Probability ร— Impact (Pร—I) risk matrix." name: risk-register argument-hint: "[project-name] [optional: focus-area]" -maturity: stable --- # Risk Register Generator diff --git a/.github/prompts/rpi.prompt.md b/.github/prompts/rpi.prompt.md index c66ea260..228f4bd9 100644 --- a/.github/prompts/rpi.prompt.md +++ b/.github/prompts/rpi.prompt.md @@ -1,7 +1,6 @@ --- description: "Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core" agent: 'rpi-agent' -maturity: stable argument-hint: "task=... [auto={true|partial|false}] [continue={1|2|3|all}] [suggest]" --- diff --git a/.github/prompts/task-implement.prompt.md b/.github/prompts/task-implement.prompt.md index 8383291d..b16fbad5 100644 --- a/.github/prompts/task-implement.prompt.md +++ b/.github/prompts/task-implement.prompt.md @@ -1,7 +1,6 @@ --- description: "Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core" agent: 'task-implementor' -maturity: stable --- # Task Implementation diff --git a/.github/prompts/task-plan.prompt.md b/.github/prompts/task-plan.prompt.md index 8da39a45..6e016431 100644 --- a/.github/prompts/task-plan.prompt.md +++ b/.github/prompts/task-plan.prompt.md @@ -1,7 +1,6 @@ --- description: "Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core" agent: 'task-planner' -maturity: stable --- # Implementation Plan diff --git a/.github/prompts/task-research.prompt.md b/.github/prompts/task-research.prompt.md index 1937f340..ed3f10f1 100644 --- a/.github/prompts/task-research.prompt.md +++ b/.github/prompts/task-research.prompt.md @@ -1,7 +1,6 @@ --- description: "Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core" agent: 'task-researcher' -maturity: stable --- # Task Research diff --git a/.github/prompts/task-review.prompt.md b/.github/prompts/task-review.prompt.md index 4525f990..9d4141af 100644 --- a/.github/prompts/task-review.prompt.md +++ b/.github/prompts/task-review.prompt.md @@ -1,7 +1,6 @@ --- description: "Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core" agent: 'task-reviewer' -maturity: stable --- # Task Review diff --git a/.github/skills/video-to-gif/SKILL.md b/.github/skills/video-to-gif/SKILL.md index c425961a..e3eeca9c 100644 --- a/.github/skills/video-to-gif/SKILL.md +++ b/.github/skills/video-to-gif/SKILL.md @@ -1,7 +1,6 @@ --- name: video-to-gif description: 'Video-to-GIF conversion skill with FFmpeg two-pass optimization - Brought to you by microsoft/hve-core' -maturity: stable --- # Video-to-GIF Conversion Skill diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 6e06ca33..32f418bb 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -87,7 +87,7 @@ All AI artifacts (agents, instructions, prompts) **MUST** target the **latest av ## Maturity Field Requirements -All AI artifacts (agents, instructions, prompts) **MUST** include a `maturity` field in frontmatter. +Maturity is defined in `.github/ai-artifacts-registry.json` and MUST NOT appear in artifact frontmatter. ### Purpose @@ -107,20 +107,22 @@ The maturity field controls which extension channel includes the artifact: ### Default for New Contributions -New artifacts **SHOULD** use `maturity: stable` unless: +New artifact registry entries **SHOULD** use `maturity: stable` unless: * The artifact is a proof-of-concept or experimental feature * The artifact requires additional testing or feedback before wide release * The contributor explicitly intends to target early adopters -### Example +### Setting Maturity -```yaml ---- -description: 'Specialized agent for security analysis' -maturity: 'stable' -tools: ['codebase', 'search'] ---- +Add or update the maturity value in the artifact's registry entry in `.github/ai-artifacts-registry.json`: + +```json +"my-artifact": { + "maturity": "stable", + "personas": ["hve-core-all"], + "tags": ["category"] +} ``` For detailed channel and lifecycle information, see [Release Process - Extension Channels](release-process.md#extension-channels-and-maturity). diff --git a/docs/contributing/custom-agents.md b/docs/contributing/custom-agents.md index 7635de2a..898c37fa 100644 --- a/docs/contributing/custom-agents.md +++ b/docs/contributing/custom-agents.md @@ -125,17 +125,6 @@ Agent files **MUST**: * **Style**: Sentence case with proper punctuation * **Example**: `'Validates contributed content for quality and compliance with hve-core standards'` -**`maturity`** (string enum, MANDATORY) - -* **Purpose**: Controls which extension channel includes this agent -* **Valid values**: - * `stable` - Production-ready, included in Stable and Pre-release channels - * `preview` - Feature-complete, included in Pre-release channel only - * `experimental` - Early development, included in Pre-release channel only - * `deprecated` - Scheduled for removal, excluded from all channels -* **Default**: New agents should use `stable` unless targeting early adopters -* **Example**: `stable` - ### Optional Fields **`tools`** (array of strings) diff --git a/docs/contributing/instructions.md b/docs/contributing/instructions.md index 55ec8341..7b6ba87d 100644 --- a/docs/contributing/instructions.md +++ b/docs/contributing/instructions.md @@ -84,17 +84,6 @@ Instruction files **MUST**: * Directory scope: `**/src/**/*.sh` * Specific paths: `**/.copilot-tracking/pr/new/**` -**`maturity`** (string enum, MANDATORY) - -* **Purpose**: Controls which extension channel includes this instruction -* **Valid values**: - * `stable` - Production-ready, included in Stable and Pre-release channels - * `preview` - Feature-complete, included in Pre-release channel only - * `experimental` - Early development, included in Pre-release channel only - * `deprecated` - Scheduled for removal, excluded from all channels -* **Default**: New instructions should use `stable` unless targeting early adopters -* **Example**: `stable` - ### Optional Fields **`version`** (string) @@ -119,7 +108,6 @@ Instruction files **MUST**: --- description: 'Required instructions for Python script implementation with type hints, docstrings, and error handling' applyTo: '**/*.py, **/*.ipynb' -maturity: 'stable' version: '1.0.0' author: 'microsoft/hve-core' lastUpdated: '2025-11-19' diff --git a/docs/contributing/prompts.md b/docs/contributing/prompts.md index 7f54be83..7a6e5d0c 100644 --- a/docs/contributing/prompts.md +++ b/docs/contributing/prompts.md @@ -72,17 +72,6 @@ Prompt files **MUST**: * `workflow` - Automated workflow/pipeline context * **Example**: `workflow` -**`maturity`** (string enum, MANDATORY) - -* **Purpose**: Controls which extension channel includes this prompt -* **Valid values**: - * `stable` - Production-ready, included in Stable and Pre-release channels - * `preview` - Feature-complete, included in Pre-release channel only - * `experimental` - Early development, included in Pre-release channel only - * `deprecated` - Scheduled for removal, excluded from all channels -* **Default**: New prompts should use `stable` unless targeting early adopters -* **Example**: `stable` - ### Optional Fields **`category`** (string enum) @@ -116,7 +105,6 @@ Prompt files **MUST**: --- description: 'Required protocol for creating Azure DevOps pull requests with work item discovery, reviewer identification, and automated linking' mode: 'workflow' -maturity: 'stable' category: 'ado' version: '1.0.0' author: 'microsoft/hve-core' @@ -429,7 +417,7 @@ Before submitting your prompt, verify: * [ ] Clear H1 title describing workflow * [ ] Overview/purpose section -* [ ] Maturity field set appropriately (see [Common Standards - Maturity](ai-artifacts-common.md#maturity-field-requirements)) +* [ ] Maturity set in registry (see [Common Standards - Maturity](ai-artifacts-common.md#maturity-field-requirements)) * [ ] Prerequisites or context section * [ ] Workflow steps with clear sequence * [ ] Success criteria defined diff --git a/docs/contributing/release-process.md b/docs/contributing/release-process.md index 0b6c2d26..0f37e9c4 100644 --- a/docs/contributing/release-process.md +++ b/docs/contributing/release-process.md @@ -138,7 +138,7 @@ The VS Code extension is published to two channels with different stability expe ### Maturity Levels -Each prompt, instruction, and agent declares a `maturity` field in its frontmatter: +Each prompt, instruction, and agent has a `maturity` value in the AI Artifacts Registry (`.github/ai-artifacts-registry.json`): | Level | Description | Included In | |----------------|-------------------------------------------------|---------------------| @@ -160,11 +160,11 @@ stateDiagram-v2 ### Contributor Guidelines -* **New contributions**: Default to `maturity: stable` unless explicitly targeting early adopters -* **Experimental work**: Use `maturity: experimental` for proof-of-concept or rapidly evolving artifacts -* **Preview promotions**: Move to `maturity: preview` when core functionality is complete -* **Stable promotions**: Move to `maturity: stable` after production validation -* **Deprecation**: Set `maturity: deprecated` before removal to provide transition time +* **New contributions**: Set `stable` in registry entry unless explicitly targeting early adopters +* **Experimental work**: Set `experimental` in registry entry for proof-of-concept or rapidly evolving artifacts +* **Preview promotions**: Set `preview` in registry entry when core functionality is complete +* **Stable promotions**: Set `stable` in registry entry after production validation +* **Deprecation**: Set `deprecated` in registry entry before removal to provide transition time --- diff --git a/docs/contributing/skills.md b/docs/contributing/skills.md index 83b5311f..34cef5c5 100644 --- a/docs/contributing/skills.md +++ b/docs/contributing/skills.md @@ -81,18 +81,12 @@ All skill files **MUST** be placed in: * **Format**: Single sentence ending with attribution * **Example**: `'Video-to-GIF conversion skill with FFmpeg two-pass optimization - Brought to you by microsoft/hve-core'` -**`maturity`** (string enum, MANDATORY) - -* **Purpose**: Controls which extension channel includes this skill -* **Valid values**: `stable`, `preview`, `experimental`, `deprecated` - ### Frontmatter Example ```yaml --- name: video-to-gif description: 'Video-to-GIF conversion skill with FFmpeg two-pass optimization - Brought to you by microsoft/hve-core' -maturity: stable --- ``` @@ -260,7 +254,6 @@ Before submitting your skill, verify: * [ ] Valid YAML between `---` delimiters * [ ] `name` field present and matches directory name * [ ] `description` field present and descriptive -* [ ] `maturity` field present with valid value ### Scripts diff --git a/scripts/linting/schemas/agent-frontmatter.schema.json b/scripts/linting/schemas/agent-frontmatter.schema.json index 2070a3fb..669b1034 100644 --- a/scripts/linting/schemas/agent-frontmatter.schema.json +++ b/scripts/linting/schemas/agent-frontmatter.schema.json @@ -15,12 +15,6 @@ "type": "string", "description": "The name of the custom agent. If not specified, the file name is used" }, - "maturity": { - "type": "string", - "enum": ["stable", "preview", "experimental", "deprecated"], - "default": "stable", - "description": "Maturity level affecting visibility in stable vs pre-release builds. Stable appears in all builds; preview/experimental only in pre-release." - }, "argument-hint": { "type": "string", "description": "Optional hint text shown in the chat input field to guide users on how to interact with the custom agent" diff --git a/scripts/linting/schemas/chatmode-frontmatter.schema.json b/scripts/linting/schemas/chatmode-frontmatter.schema.json index 7b141c5b..e2c9b3f3 100644 --- a/scripts/linting/schemas/chatmode-frontmatter.schema.json +++ b/scripts/linting/schemas/chatmode-frontmatter.schema.json @@ -11,12 +11,6 @@ "minLength": 1, "description": "Concise explanation of chatmode functionality (10-200 characters)" }, - "maturity": { - "type": "string", - "enum": ["stable", "preview", "experimental", "deprecated"], - "default": "stable", - "description": "Maturity level affecting visibility in stable vs pre-release builds. Stable appears in all builds; preview/experimental only in pre-release." - }, "tools": { "type": "array", "items": { diff --git a/scripts/linting/schemas/instruction-frontmatter.schema.json b/scripts/linting/schemas/instruction-frontmatter.schema.json index 82f980e2..a5986b95 100644 --- a/scripts/linting/schemas/instruction-frontmatter.schema.json +++ b/scripts/linting/schemas/instruction-frontmatter.schema.json @@ -19,12 +19,6 @@ "applyTo": { "type": "string", "description": "Optional glob pattern that defines which files the instructions should be applied to automatically, relative to the workspace root. Use ** to apply to all files. If not specified, instructions are not applied automatically." - }, - "maturity": { - "type": "string", - "enum": ["stable", "preview", "experimental", "deprecated"], - "default": "stable", - "description": "Maturity level affecting visibility in stable vs pre-release builds. Stable appears in all builds; preview/experimental only in pre-release." } }, "additionalProperties": false diff --git a/scripts/linting/schemas/prompt-frontmatter.schema.json b/scripts/linting/schemas/prompt-frontmatter.schema.json index 2e81e8a1..2331b4ea 100644 --- a/scripts/linting/schemas/prompt-frontmatter.schema.json +++ b/scripts/linting/schemas/prompt-frontmatter.schema.json @@ -34,12 +34,6 @@ "type": "string" }, "description": "List of tool names or tool set names available for this prompt. Can include built-in tools, MCP tools (/*), or extension tools." - }, - "maturity": { - "type": "string", - "enum": ["stable", "preview", "experimental", "deprecated"], - "default": "stable", - "description": "Maturity level affecting visibility in stable vs pre-release builds. Stable appears in all builds; preview/experimental only in pre-release." } }, "additionalProperties": true diff --git a/scripts/linting/schemas/skill-frontmatter.schema.json b/scripts/linting/schemas/skill-frontmatter.schema.json index a8132f19..93d4f5ad 100644 --- a/scripts/linting/schemas/skill-frontmatter.schema.json +++ b/scripts/linting/schemas/skill-frontmatter.schema.json @@ -4,7 +4,7 @@ "title": "Skill File Frontmatter Schema", "description": "Frontmatter schema for SKILL.md files in skill directories", "type": "object", - "required": ["name", "description", "maturity"], + "required": ["name", "description"], "properties": { "name": { "type": "string", @@ -16,11 +16,6 @@ "type": "string", "minLength": 1, "description": "Brief description of the skill" - }, - "maturity": { - "type": "string", - "enum": ["stable", "preview", "experimental", "deprecated"], - "description": "Maturity level of the skill" } }, "additionalProperties": false From d323d79cd5ed60fc089236fd96d52cdd827e024e Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 16:11:58 +0100 Subject: [PATCH 07/62] feat(extension): add collection-based multi-package build system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation: - Add collection.schema.json (JSON Schema 2020-12) - Create hve-core-all and developer collection manifests - Seed 3 registry entries with cross-persona tags Prepare-Extension enhancements: - Migrate maturity filtering from YAML frontmatter to registry - Add Get-RegistryData, Get-DiscoveredSkills, chatSkills support - Add collection filtering - Add BFS handoff resolution (Resolve-HandoffDependencies) - Accept -Collection parameter for filtered builds Package-Extension enhancements: - Add skills to Get-PackagingDirectorySpec (5 specs) - Add -Collection parameter with Copy-CollectionArtifacts - Add CollectionId to Get-ExtensionOutputPath Tests: - Add 25 new Pester tests for collection functions - Update existing tests for registry-based maturity Refs #433 ๐Ÿš€ - Generated by Copilot --- .github/ai-artifacts-registry.json | 21 +- extension/.vscodeignore | 1 + .../collections/developer.collection.json | 10 + .../collections/hve-core-all.collection.json | 10 + scripts/extension/Package-Extension.ps1 | 190 +++++- scripts/extension/Prepare-Extension.ps1 | 631 +++++++++++++++++- .../linting/schemas/collection.schema.json | 79 +++ .../extension/Package-Extension.Tests.ps1 | 28 +- .../extension/Prepare-Extension.Tests.ps1 | 489 +++++++++++++- 9 files changed, 1376 insertions(+), 83 deletions(-) create mode 100644 extension/collections/developer.collection.json create mode 100644 extension/collections/hve-core-all.collection.json create mode 100644 scripts/linting/schemas/collection.schema.json diff --git a/.github/ai-artifacts-registry.json b/.github/ai-artifacts-registry.json index ab029699..75dedd90 100644 --- a/.github/ai-artifacts-registry.json +++ b/.github/ai-artifacts-registry.json @@ -269,7 +269,12 @@ "rpi-agent": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer", + "tpm", + "devops", + "architect", + "technical-writer" ], "tags": [ "rpi", @@ -556,7 +561,12 @@ "rpi": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer", + "tpm", + "devops", + "architect", + "technical-writer" ], "tags": [ "rpi" @@ -681,7 +691,12 @@ "markdown": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer", + "tpm", + "devops", + "architect", + "technical-writer" ], "tags": [ "documentation" diff --git a/extension/.vscodeignore b/extension/.vscodeignore index bf53d93b..926b9083 100644 --- a/extension/.vscodeignore +++ b/extension/.vscodeignore @@ -5,6 +5,7 @@ !.github/prompts/** !.github/instructions/** !.github/agents/** +!.github/skills/** !docs/templates/** !scripts/dev-tools/** !scripts/lib/Modules/CIHelpers.psm1 diff --git a/extension/collections/developer.collection.json b/extension/collections/developer.collection.json new file mode 100644 index 00000000..0e85c429 --- /dev/null +++ b/extension/collections/developer.collection.json @@ -0,0 +1,10 @@ +{ + "$schema": "../../scripts/linting/schemas/collection.schema.json", + "id": "developer", + "name": "hve-developer", + "displayName": "HVE Core - Developer Edition", + "description": "AI-powered coding agents and prompts curated for software engineers", + "personas": [ + "developer" + ] +} \ No newline at end of file diff --git a/extension/collections/hve-core-all.collection.json b/extension/collections/hve-core-all.collection.json new file mode 100644 index 00000000..7e3fb611 --- /dev/null +++ b/extension/collections/hve-core-all.collection.json @@ -0,0 +1,10 @@ +{ + "$schema": "../../scripts/linting/schemas/collection.schema.json", + "id": "hve-core-all", + "name": "hve-core", + "displayName": "HVE Core", + "description": "AI-powered chat agents, prompts, and instructions for hybrid virtual environments", + "personas": [ + "hve-core-all" + ] +} \ No newline at end of file diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index 4222b515..1ac84113 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -27,6 +27,11 @@ Optional. When specified, packages the extension for VS Code Marketplace pre-release channel. Uses vsce --pre-release flag which marks the extension for the pre-release track. +.PARAMETER Collection + Optional. Path to a collection manifest JSON file. When specified, only + collection-filtered artifacts are copied and the output filename uses the + collection ID. + .EXAMPLE ./Package-Extension.ps1 # Packages using version from package.json @@ -68,7 +73,10 @@ param( [string]$ChangelogPath = "", [Parameter(Mandatory = $false)] - [switch]$PreRelease + [switch]$PreRelease, + + [Parameter(Mandatory = $false)] + [string]$Collection = "" ) $ErrorActionPreference = 'Stop' @@ -123,6 +131,8 @@ function Get-ExtensionOutputPath { The name of the extension (from package.json). .PARAMETER PackageVersion The version string to use in the filename. + .PARAMETER CollectionId + Optional collection identifier to use instead of the extension name in the filename. .OUTPUTS String path to the expected .vsix file. #> @@ -136,10 +146,17 @@ function Get-ExtensionOutputPath { [string]$ExtensionName, [Parameter(Mandatory = $true)] - [string]$PackageVersion + [string]$PackageVersion, + + [Parameter(Mandatory = $false)] + [string]$CollectionId = "" ) - $vsixFileName = "$ExtensionName-$PackageVersion.vsix" + $vsixFileName = if ($CollectionId -and $CollectionId -ne "") { + "$CollectionId-$PackageVersion.vsix" + } else { + "$ExtensionName-$PackageVersion.vsix" + } return Join-Path $ExtensionDirectory $vsixFileName } @@ -422,6 +439,11 @@ function Get-PackagingDirectorySpec { Destination = Join-Path $ExtensionDirectory ".github" IsFile = $false }, + @{ + Source = Join-Path $RepoRoot ".github/skills" + Destination = Join-Path $ExtensionDirectory ".github/skills" + IsFile = $false + }, @{ Source = Join-Path $RepoRoot "scripts/dev-tools" Destination = Join-Path $ExtensionDirectory "scripts/dev-tools" @@ -444,6 +466,94 @@ function Get-PackagingDirectorySpec { #region I/O Functions +function Copy-CollectionArtifacts { + <# + .SYNOPSIS + Copies only collection-filtered artifacts to the extension directory. + .DESCRIPTION + Reads the prepared package.json to determine which artifacts were selected + by collection filtering, then copies only those files instead of the entire + .github directory. Always includes copilot-instructions.md. + .PARAMETER RepoRoot + Absolute path to the repository root. + .PARAMETER ExtensionDirectory + Absolute path to the extension directory. + .PARAMETER PrepareResult + Result hashtable from Invoke-PrepareExtension (unused directly, kept for API consistency). + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$RepoRoot, + + [Parameter(Mandatory = $true)] + [string]$ExtensionDirectory, + + [Parameter(Mandatory = $true)] + [hashtable]$PrepareResult + ) + + $preparedPkgJson = Get-Content -Path (Join-Path $ExtensionDirectory "package.json") -Raw | ConvertFrom-Json + + # Copy filtered agents + if ($preparedPkgJson.contributes.chatAgents) { + $agentsDestDir = Join-Path $ExtensionDirectory ".github/agents" + New-Item -Path $agentsDestDir -ItemType Directory -Force | Out-Null + foreach ($agent in $preparedPkgJson.contributes.chatAgents) { + $srcPath = Join-Path $RepoRoot ($agent.path.TrimStart('./')) + if (Test-Path $srcPath) { + Copy-Item -Path $srcPath -Destination $agentsDestDir -Force + } + } + } + + # Copy filtered prompts + if ($preparedPkgJson.contributes.chatPromptFiles) { + foreach ($prompt in $preparedPkgJson.contributes.chatPromptFiles) { + $srcPath = Join-Path $RepoRoot ($prompt.path.TrimStart('./')) + $destPath = Join-Path $ExtensionDirectory ($prompt.path.TrimStart('./')) + $destDir = Split-Path $destPath -Parent + New-Item -Path $destDir -ItemType Directory -Force | Out-Null + if (Test-Path $srcPath) { + Copy-Item -Path $srcPath -Destination $destPath -Force + } + } + } + + # Copy filtered instructions + if ($preparedPkgJson.contributes.chatInstructions) { + foreach ($instr in $preparedPkgJson.contributes.chatInstructions) { + $srcPath = Join-Path $RepoRoot ($instr.path.TrimStart('./')) + $destPath = Join-Path $ExtensionDirectory ($instr.path.TrimStart('./')) + $destDir = Split-Path $destPath -Parent + New-Item -Path $destDir -ItemType Directory -Force | Out-Null + if (Test-Path $srcPath) { + Copy-Item -Path $srcPath -Destination $destPath -Force + } + } + } + + # Copy filtered skills + if ($preparedPkgJson.contributes.chatSkills) { + foreach ($skill in $preparedPkgJson.contributes.chatSkills) { + $srcPath = Join-Path $RepoRoot ($skill.path.TrimStart('./')) + $destPath = Join-Path $ExtensionDirectory ($skill.path.TrimStart('./')) + if (Test-Path $srcPath) { + Copy-Item -Path $srcPath -Destination $destPath -Recurse -Force + } + } + } + + # Always copy copilot-instructions.md (needed by all collections) + $copilotInstr = Join-Path $RepoRoot ".github/copilot-instructions.md" + $copilotDest = Join-Path $ExtensionDirectory ".github/copilot-instructions.md" + if (Test-Path $copilotInstr) { + $copilotDestDir = Split-Path $copilotDest -Parent + New-Item -Path $copilotDestDir -ItemType Directory -Force | Out-Null + Copy-Item -Path $copilotInstr -Destination $copilotDest -Force + } +} + function Invoke-VsceCommand { <# .SYNOPSIS @@ -597,6 +707,10 @@ function Invoke-PackageExtension { Optional path to changelog file to include in package. .PARAMETER PreRelease Switch to mark the package as a pre-release version. + .PARAMETER Collection + Optional path to a collection manifest JSON file. When specified, only + collection-filtered artifacts are copied and the output filename uses the + collection ID. .OUTPUTS Hashtable with Success, OutputPath, Version, and ErrorMessage properties. #> @@ -621,7 +735,10 @@ function Invoke-PackageExtension { [string]$ChangelogPath = "", [Parameter(Mandatory = $false)] - [switch]$PreRelease + [switch]$PreRelease, + + [Parameter(Mandatory = $false)] + [string]$Collection = "" ) $dirsToClean = @(".github", "docs", "scripts") @@ -683,6 +800,13 @@ function Invoke-PackageExtension { $versionWasModified = $true } + # Extract collection ID for output naming + $collectionId = $null + if ($Collection -and $Collection -ne "") { + $collectionContent = Get-Content -Path $Collection -Raw | ConvertFrom-Json + $collectionId = $collectionContent.id + } + # Handle changelog if provided if ($ChangelogPath -and $ChangelogPath -ne "") { Write-Host "" @@ -713,20 +837,51 @@ function Invoke-PackageExtension { # Get and execute copy specifications $copySpecs = Get-PackagingDirectorySpec -RepoRoot $RepoRoot -ExtensionDirectory $ExtensionDirectory - foreach ($spec in $copySpecs) { - $specName = Split-Path $spec.Source -Leaf - Write-Host " Copying $specName..." -ForegroundColor Gray - - if ($spec.IsFile) { - $parentDir = Split-Path $spec.Destination -Parent - New-Item -Path $parentDir -ItemType Directory -Force | Out-Null - Copy-Item -Path $spec.Source -Destination $spec.Destination -Force - } else { - $parentDir = Split-Path $spec.Destination -Parent - if (-not (Test-Path $parentDir)) { + + if ($Collection -and $Collection -ne "") { + # Collection mode: copy only filtered artifacts for .github content + Write-Host " Using collection-filtered artifact copy..." -ForegroundColor Gray + + # Copy non-.github specs normally + foreach ($spec in $copySpecs) { + if ($spec.Source -like "*/.github*" -or $spec.Source -like "*\.github*") { + continue + } + $specName = Split-Path $spec.Source -Leaf + Write-Host " Copying $specName..." -ForegroundColor Gray + + if ($spec.IsFile) { + $parentDir = Split-Path $spec.Destination -Parent + New-Item -Path $parentDir -ItemType Directory -Force | Out-Null + Copy-Item -Path $spec.Source -Destination $spec.Destination -Force + } else { + $parentDir = Split-Path $spec.Destination -Parent + if (-not (Test-Path $parentDir)) { + New-Item -Path $parentDir -ItemType Directory -Force | Out-Null + } + Copy-Item -Path $spec.Source -Destination $spec.Destination -Recurse -Force + } + } + + # Copy collection-specific artifacts + Copy-CollectionArtifacts -RepoRoot $RepoRoot -ExtensionDirectory $ExtensionDirectory -PrepareResult @{} + } else { + # Full mode: copy everything as before + foreach ($spec in $copySpecs) { + $specName = Split-Path $spec.Source -Leaf + Write-Host " Copying $specName..." -ForegroundColor Gray + + if ($spec.IsFile) { + $parentDir = Split-Path $spec.Destination -Parent New-Item -Path $parentDir -ItemType Directory -Force | Out-Null + Copy-Item -Path $spec.Source -Destination $spec.Destination -Force + } else { + $parentDir = Split-Path $spec.Destination -Parent + if (-not (Test-Path $parentDir)) { + New-Item -Path $parentDir -ItemType Directory -Force | Out-Null + } + Copy-Item -Path $spec.Source -Destination $spec.Destination -Recurse -Force } - Copy-Item -Path $spec.Source -Destination $spec.Destination -Recurse -Force } } @@ -821,7 +976,8 @@ try { -Version $Version ` -DevPatchNumber $DevPatchNumber ` -ChangelogPath $ChangelogPath ` - -PreRelease:$PreRelease + -PreRelease:$PreRelease ` + -Collection $Collection if (-not $result.Success) { Write-Error $result.ErrorMessage diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index c05be311..eb4343e5 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -53,7 +53,10 @@ param( [string]$Channel = 'Stable', [Parameter(Mandatory = $false)] - [switch]$DryRun + [switch]$DryRun, + + [Parameter(Mandatory = $false)] + [string]$Collection = "" ) $ErrorActionPreference = 'Stop' @@ -88,19 +91,377 @@ function Get-AllowedMaturities { return @('stable') } +function Get-RegistryData { + <# + .SYNOPSIS + Loads the AI artifacts registry JSON file. + .DESCRIPTION + Reads and parses the AI artifacts registry JSON file into a hashtable + containing artifact metadata keyed by type (agents, prompts, instructions, skills). + .PARAMETER RegistryPath + Path to the ai-artifacts-registry.json file. + .OUTPUTS + [hashtable] Parsed registry data with keys: agents, prompts, instructions, skills, personas, version. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$RegistryPath + ) + + if (-not (Test-Path $RegistryPath)) { + throw "AI artifacts registry not found: $RegistryPath" + } + + $content = Get-Content -Path $RegistryPath -Raw + return $content | ConvertFrom-Json -AsHashtable +} + +function Get-CollectionManifest { + <# + .SYNOPSIS + Loads a collection manifest JSON file. + .DESCRIPTION + Reads and parses a collection manifest JSON file that defines persona-based + artifact filtering rules for extension packaging. + .PARAMETER CollectionPath + Path to the collection manifest JSON file. + .OUTPUTS + [hashtable] Parsed collection manifest with id, name, displayName, description, personas, and optional include/exclude. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$CollectionPath + ) + + if (-not (Test-Path $CollectionPath)) { + throw "Collection manifest not found: $CollectionPath" + } + + $content = Get-Content -Path $CollectionPath -Raw + return $content | ConvertFrom-Json -AsHashtable +} + +function Test-GlobMatch { + <# + .SYNOPSIS + Tests whether a name matches any of the provided glob patterns. + .DESCRIPTION + Uses PowerShell's -like operator to test glob pattern matching with + * (any characters) and ? (single character) wildcards. + .PARAMETER Name + The artifact name to test against patterns. + .PARAMETER Patterns + Array of glob patterns to match against. + .OUTPUTS + [bool] True if name matches any pattern, false otherwise. + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string]$Name, + + [Parameter(Mandatory = $true)] + [string[]]$Patterns + ) + + foreach ($pattern in $Patterns) { + if ($Name -like $pattern) { + return $true + } + } + return $false +} + +function Get-CollectionArtifacts { + <# + .SYNOPSIS + Filters registry artifacts by collection persona, maturity, and glob patterns. + .DESCRIPTION + Applies collection-level filtering to the artifact registry, returning artifact + names that match the collection's persona requirements, allowed maturities, + and optional include/exclude glob patterns. + .PARAMETER Registry + AI artifacts registry hashtable. + .PARAMETER Collection + Collection manifest hashtable with personas and optional include/exclude. + .PARAMETER AllowedMaturities + Array of maturity levels to include. + .OUTPUTS + [hashtable] With Agents, Prompts, Instructions, Skills arrays of matching artifact names. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$Registry, + + [Parameter(Mandatory = $true)] + [hashtable]$Collection, + + [Parameter(Mandatory = $true)] + [string[]]$AllowedMaturities + ) + + $result = @{ + Agents = @() + Prompts = @() + Instructions = @() + Skills = @() + } + + $collectionPersonas = $Collection.personas + + foreach ($type in @('agents', 'prompts', 'instructions', 'skills')) { + if (-not $Registry.ContainsKey($type)) { continue } + + $includePatterns = @() + $excludePatterns = @() + if ($Collection.ContainsKey('include') -and $Collection.include.ContainsKey($type)) { + $includePatterns = $Collection.include[$type] + } + if ($Collection.ContainsKey('exclude') -and $Collection.exclude.ContainsKey($type)) { + $excludePatterns = $Collection.exclude[$type] + } + + foreach ($name in $Registry[$type].Keys) { + $entry = $Registry[$type][$name] + + # Persona filter: artifact must belong to at least one collection persona + $personaMatch = $false + foreach ($persona in $entry.personas) { + if ($collectionPersonas -contains $persona) { + $personaMatch = $true + break + } + } + if (-not $personaMatch) { continue } + + # Maturity filter + if ($AllowedMaturities -notcontains $entry.maturity) { continue } + + # Include glob filter (if specified) + if ($includePatterns.Count -gt 0 -and -not (Test-GlobMatch -Name $name -Patterns $includePatterns)) { + continue + } + + # Exclude glob filter + if ($excludePatterns.Count -gt 0 -and (Test-GlobMatch -Name $name -Patterns $excludePatterns)) { + continue + } + + $capitalType = @{ agents = 'Agents'; prompts = 'Prompts'; instructions = 'Instructions'; skills = 'Skills' }[$type] + $result[$capitalType] += $name + } + } + + return $result +} + +function Resolve-HandoffDependencies { + <# + .SYNOPSIS + Resolves transitive agent handoff dependencies using BFS traversal. + .DESCRIPTION + Starting from seed agents, performs breadth-first traversal of agent handoff + declarations in YAML frontmatter to compute the transitive closure of + all agents reachable through handoff chains. + .PARAMETER SeedAgents + Initial agent names to start BFS from. + .PARAMETER AgentsDir + Path to the agents directory containing .agent.md files. + .PARAMETER AllowedMaturities + Array of maturity levels to include. + .PARAMETER Registry + AI artifacts registry hashtable for maturity lookup. + .OUTPUTS + [string[]] Complete set of agent names including seed agents and all transitive handoff targets. + #> + [CmdletBinding()] + [OutputType([string[]])] + param( + [Parameter(Mandatory = $true)] + [string[]]$SeedAgents, + + [Parameter(Mandatory = $true)] + [string]$AgentsDir, + + [Parameter(Mandatory = $true)] + [string[]]$AllowedMaturities, + + [Parameter(Mandatory = $false)] + [hashtable]$Registry = @{} + ) + + $visited = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + $queue = [System.Collections.Generic.Queue[string]]::new() + + foreach ($agent in $SeedAgents) { + if ($visited.Add($agent)) { + $queue.Enqueue($agent) + } + } + + while ($queue.Count -gt 0) { + $current = $queue.Dequeue() + $agentFile = Join-Path $AgentsDir "$current.agent.md" + + if (-not (Test-Path $agentFile)) { + Write-Warning "Handoff target agent file not found: $agentFile" + continue + } + + # Check maturity from registry + $maturity = "stable" + if ($Registry.Count -gt 0 -and $Registry.ContainsKey('agents') -and $Registry.agents.ContainsKey($current)) { + $maturity = $Registry.agents[$current].maturity + } + if ($AllowedMaturities -notcontains $maturity) { continue } + + # Parse handoffs from frontmatter + $content = Get-Content -Path $agentFile -Raw + if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') { + $yamlContent = $Matches[1] -replace '\r\n', "`n" -replace '\r', "`n" + try { + $data = ConvertFrom-Yaml -Yaml $yamlContent + if ($data.ContainsKey('handoffs') -and $data.handoffs -is [System.Collections.IEnumerable] -and $data.handoffs -isnot [string]) { + foreach ($handoff in $data.handoffs) { + if ($handoff -is [string] -and $visited.Add($handoff)) { + $queue.Enqueue($handoff) + } + } + } + } + catch { + Write-Warning "Failed to parse handoffs from $current.agent.md: $_" + } + } + } + + return @($visited) +} + +function Resolve-RequiresDependencies { + <# + .SYNOPSIS + Resolves transitive artifact dependencies from registry requires blocks. + .DESCRIPTION + Walks the requires blocks in agent registry entries to compute the + complete set of dependent artifacts across all types (agents, prompts, + instructions, skills) using BFS for transitive agent dependencies. + .PARAMETER ArtifactNames + Hashtable with initial artifact name arrays keyed by type (agents, prompts, instructions, skills). + .PARAMETER Registry + AI artifacts registry hashtable. + .PARAMETER AllowedMaturities + Array of maturity levels to include. + .OUTPUTS + [hashtable] With Agents, Prompts, Instructions, Skills arrays containing resolved names. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$ArtifactNames, + + [Parameter(Mandatory = $true)] + [hashtable]$Registry, + + [Parameter(Mandatory = $true)] + [string[]]$AllowedMaturities + ) + + $resolved = @{ + Agents = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + Prompts = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + Instructions = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + Skills = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + } + + $typeMap = @{ + agents = 'Agents' + prompts = 'Prompts' + instructions = 'Instructions' + skills = 'Skills' + } + + # Seed with initial artifact names + foreach ($type in @('agents', 'prompts', 'instructions', 'skills')) { + $capitalType = $typeMap[$type] + if ($ArtifactNames.ContainsKey($type)) { + foreach ($name in $ArtifactNames[$type]) { + $null = $resolved[$capitalType].Add($name) + } + } + } + + # Walk requires for agents (only agents have requires blocks) + $processedAgents = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + $agentQueue = [System.Collections.Generic.Queue[string]]::new() + + foreach ($agent in $resolved.Agents) { + $agentQueue.Enqueue($agent) + } + + while ($agentQueue.Count -gt 0) { + $current = $agentQueue.Dequeue() + if (-not $processedAgents.Add($current)) { continue } + + if (-not $Registry.ContainsKey('agents') -or -not $Registry.agents.ContainsKey($current)) { continue } + + $entry = $Registry.agents[$current] + if (-not $entry.ContainsKey('requires')) { continue } + + $requires = $entry.requires + + foreach ($type in @('agents', 'prompts', 'instructions', 'skills')) { + if (-not $requires.ContainsKey($type)) { continue } + $capitalType = $typeMap[$type] + + foreach ($dep in $requires[$type]) { + # Check maturity of dependency + if ($Registry.ContainsKey($type) -and $Registry[$type].ContainsKey($dep)) { + $depMaturity = $Registry[$type][$dep].maturity + if ($AllowedMaturities -notcontains $depMaturity) { continue } + } + + if ($resolved[$capitalType].Add($dep)) { + if ($type -eq 'agents') { + $agentQueue.Enqueue($dep) + } + } + } + } + } + + # Convert HashSets to arrays + return @{ + Agents = @($resolved.Agents) + Prompts = @($resolved.Prompts) + Instructions = @($resolved.Instructions) + Skills = @($resolved.Skills) + } +} + function Get-FrontmatterData { <# .SYNOPSIS - Extracts description and maturity from YAML frontmatter. + Extracts description from YAML frontmatter. .DESCRIPTION Function that parses YAML frontmatter from a markdown file - and returns a hashtable with description and maturity values. + and returns a hashtable with the description value. .PARAMETER FilePath Path to the markdown file to parse. .PARAMETER FallbackDescription Default description if none found in frontmatter. .OUTPUTS - [hashtable] With description and maturity keys. + [hashtable] With description key. #> [CmdletBinding()] [OutputType([hashtable])] @@ -114,7 +475,6 @@ function Get-FrontmatterData { $content = Get-Content -Path $FilePath -Raw $description = "" - $maturity = "stable" if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') { $yamlContent = $Matches[1] -replace '\r\n', "`n" -replace '\r', "`n" @@ -123,9 +483,6 @@ function Get-FrontmatterData { if ($data.ContainsKey('description')) { $description = $data.description } - if ($data.ContainsKey('maturity')) { - $maturity = $data.maturity - } } catch { Write-Warning "Failed to parse YAML frontmatter in $(Split-Path -Leaf $FilePath): $_" @@ -134,7 +491,6 @@ function Get-FrontmatterData { return @{ description = if ($description) { $description } else { $FallbackDescription } - maturity = $maturity } } @@ -196,7 +552,7 @@ function Get-DiscoveredAgents { Discovers chat agent files from the agents directory. .DESCRIPTION Discovery function that scans the agents directory for .agent.md files, - extracts frontmatter data, filters by maturity and exclusion list, + extracts frontmatter description, filters by registry maturity and exclusion list, and returns structured agent objects. .PARAMETER AgentsDir Path to the agents directory. @@ -204,6 +560,8 @@ function Get-DiscoveredAgents { Array of maturity levels to include. .PARAMETER ExcludedAgents Array of agent names to exclude from packaging. + .PARAMETER Registry + AI artifacts registry hashtable for maturity lookup. .OUTPUTS [hashtable] With Agents array, Skipped array, and DirectoryExists bool. #> @@ -217,7 +575,10 @@ function Get-DiscoveredAgents { [string[]]$AllowedMaturities, [Parameter(Mandatory = $false)] - [string[]]$ExcludedAgents = @() + [string[]]$ExcludedAgents = @(), + + [Parameter(Mandatory = $false)] + [hashtable]$Registry = @{} ) $result = @{ @@ -240,8 +601,13 @@ function Get-DiscoveredAgents { continue } + # Determine maturity from registry if available, else default to stable + $maturity = "stable" + if ($Registry.Count -gt 0 -and $Registry.ContainsKey('agents') -and $Registry.agents.ContainsKey($agentName)) { + $maturity = $Registry.agents[$agentName].maturity + } + $frontmatter = Get-FrontmatterData -FilePath $agentFile.FullName -FallbackDescription "AI agent for $agentName" - $maturity = $frontmatter.maturity if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $agentName; Reason = "maturity: $maturity" } @@ -264,14 +630,16 @@ function Get-DiscoveredPrompts { Discovers prompt files from the prompts directory. .DESCRIPTION Discovery function that scans the prompts directory for .prompt.md files, - extracts frontmatter data, filters by maturity, and returns structured - prompt objects with relative paths. + extracts frontmatter description, filters by registry maturity, and returns + structured prompt objects with relative paths. .PARAMETER PromptsDir Path to the prompts directory. .PARAMETER GitHubDir Path to the .github directory for relative path calculation. .PARAMETER AllowedMaturities Array of maturity levels to include. + .PARAMETER Registry + AI artifacts registry hashtable for maturity lookup. .OUTPUTS [hashtable] With Prompts array, Skipped array, and DirectoryExists bool. #> @@ -285,7 +653,10 @@ function Get-DiscoveredPrompts { [string]$GitHubDir, [Parameter(Mandatory = $true)] - [string[]]$AllowedMaturities + [string[]]$AllowedMaturities, + + [Parameter(Mandatory = $false)] + [hashtable]$Registry = @{} ) $result = @{ @@ -303,8 +674,14 @@ function Get-DiscoveredPrompts { foreach ($promptFile in $promptFiles) { $promptName = $promptFile.BaseName -replace '\.prompt$', '' $displayName = ($promptName -replace '-', ' ') -replace '(\b\w)', { $_.Groups[1].Value.ToUpper() } + + # Determine maturity from registry if available, else default to stable + $maturity = "stable" + if ($Registry.Count -gt 0 -and $Registry.ContainsKey('prompts') -and $Registry.prompts.ContainsKey($promptName)) { + $maturity = $Registry.prompts[$promptName].maturity + } + $frontmatter = Get-FrontmatterData -FilePath $promptFile.FullName -FallbackDescription "Prompt for $displayName" - $maturity = $frontmatter.maturity if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $promptName; Reason = "maturity: $maturity" } @@ -329,14 +706,16 @@ function Get-DiscoveredInstructions { Discovers instruction files from the instructions directory. .DESCRIPTION Discovery function that scans the instructions directory for .instructions.md files, - extracts frontmatter data, filters by maturity, and returns structured - instruction objects with normalized paths. + extracts frontmatter description, filters by registry maturity, and returns + structured instruction objects with normalized paths. .PARAMETER InstructionsDir Path to the instructions directory. .PARAMETER GitHubDir Path to the .github directory for relative path calculation. .PARAMETER AllowedMaturities Array of maturity levels to include. + .PARAMETER Registry + AI artifacts registry hashtable for maturity lookup. .OUTPUTS [hashtable] With Instructions array, Skipped array, and DirectoryExists bool. #> @@ -350,7 +729,10 @@ function Get-DiscoveredInstructions { [string]$GitHubDir, [Parameter(Mandatory = $true)] - [string[]]$AllowedMaturities + [string[]]$AllowedMaturities, + + [Parameter(Mandatory = $false)] + [hashtable]$Registry = @{} ) $result = @{ @@ -369,8 +751,16 @@ function Get-DiscoveredInstructions { $baseName = $instrFile.BaseName -replace '\.instructions$', '' $instrName = "$baseName-instructions" $displayName = ($baseName -replace '-', ' ') -replace '(\b\w)', { $_.Groups[1].Value.ToUpper() } + + # Determine maturity from registry using relative path key + $relPath = [System.IO.Path]::GetRelativePath($InstructionsDir, $instrFile.FullName) -replace '\\', '/' + $registryKey = $relPath -replace '\.instructions\.md$', '' + $maturity = "stable" + if ($Registry.Count -gt 0 -and $Registry.ContainsKey('instructions') -and $Registry.instructions.ContainsKey($registryKey)) { + $maturity = $Registry.instructions[$registryKey].maturity + } + $frontmatter = Get-FrontmatterData -FilePath $instrFile.FullName -FallbackDescription "Instructions for $displayName" - $maturity = $frontmatter.maturity if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $instrName; Reason = "maturity: $maturity" } @@ -390,13 +780,87 @@ function Get-DiscoveredInstructions { return $result } +function Get-DiscoveredSkills { + <# + .SYNOPSIS + Discovers skill packages from the skills directory. + .DESCRIPTION + Discovery function that scans the skills directory for subdirectories + containing SKILL.md files, filters by registry maturity, and returns + structured skill objects. + .PARAMETER SkillsDir + Path to the skills directory. + .PARAMETER AllowedMaturities + Array of maturity levels to include. + .PARAMETER Registry + AI artifacts registry hashtable for maturity lookup. + .OUTPUTS + [hashtable] With Skills array, Skipped array, and DirectoryExists bool. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string]$SkillsDir, + + [Parameter(Mandatory = $true)] + [string[]]$AllowedMaturities, + + [Parameter(Mandatory = $false)] + [hashtable]$Registry = @{} + ) + + $result = @{ + Skills = @() + Skipped = @() + DirectoryExists = (Test-Path $SkillsDir) + } + + if (-not $result.DirectoryExists) { + return $result + } + + $skillDirs = Get-ChildItem -Path $SkillsDir -Directory | Sort-Object Name + + foreach ($skillDir in $skillDirs) { + $skillName = $skillDir.Name + $skillFile = Join-Path $skillDir.FullName "SKILL.md" + + if (-not (Test-Path $skillFile)) { + $result.Skipped += @{ Name = $skillName; Reason = 'missing SKILL.md' } + continue + } + + $maturity = "stable" + if ($Registry.Count -gt 0 -and $Registry.ContainsKey('skills') -and $Registry.skills.ContainsKey($skillName)) { + $maturity = $Registry.skills[$skillName].maturity + } + + if ($AllowedMaturities -notcontains $maturity) { + $result.Skipped += @{ Name = $skillName; Reason = "maturity: $maturity" } + continue + } + + $frontmatter = Get-FrontmatterData -FilePath $skillFile -FallbackDescription "Skill for $skillName" + + $result.Skills += [PSCustomObject]@{ + name = $skillName + path = "./.github/skills/$skillName" + description = $frontmatter.description + } + } + + return $result +} + function Update-PackageJsonContributes { <# .SYNOPSIS Updates package.json contributes section with discovered components. .DESCRIPTION Pure function that takes a package.json object and discovered components, - returning a new object with the contributes section updated. + returning a new object with the contributes section updated. Handles + chatAgents, chatPromptFiles, chatInstructions, and chatSkills. .PARAMETER PackageJson The package.json object to update. .PARAMETER ChatAgents @@ -405,6 +869,8 @@ function Update-PackageJsonContributes { Array of discovered prompt objects. .PARAMETER ChatInstructions Array of discovered instruction objects. + .PARAMETER ChatSkills + Array of discovered skill objects. .OUTPUTS [PSCustomObject] Updated package.json object. #> @@ -424,7 +890,11 @@ function Update-PackageJsonContributes { [Parameter(Mandatory = $true)] [AllowEmptyCollection()] - [array]$ChatInstructions + [array]$ChatInstructions, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [array]$ChatSkills ) # Clone the object to avoid modifying the original @@ -454,6 +924,12 @@ function Update-PackageJsonContributes { $updated.contributes.chatInstructions = $ChatInstructions } + if ($null -eq $updated.contributes.chatSkills) { + $updated.contributes | Add-Member -NotePropertyName "chatSkills" -NotePropertyValue $ChatSkills -Force + } else { + $updated.contributes.chatSkills = $ChatSkills + } + return $updated } @@ -474,11 +950,13 @@ function New-PrepareResult { Number of prompts discovered and included. .PARAMETER InstructionCount Number of instructions discovered and included. + .PARAMETER SkillCount + Number of skills discovered and included. .PARAMETER ErrorMessage Error description when Success is false. .OUTPUTS Hashtable with Success, Version, AgentCount, PromptCount, - InstructionCount, and ErrorMessage properties. + InstructionCount, SkillCount, and ErrorMessage properties. #> [CmdletBinding()] [OutputType([hashtable])] @@ -498,6 +976,9 @@ function New-PrepareResult { [Parameter(Mandatory = $false)] [int]$InstructionCount = 0, + [Parameter(Mandatory = $false)] + [int]$SkillCount = 0, + [Parameter(Mandatory = $false)] [string]$ErrorMessage = "" ) @@ -508,6 +989,7 @@ function New-PrepareResult { AgentCount = $AgentCount PromptCount = $PromptCount InstructionCount = $InstructionCount + SkillCount = $SkillCount ErrorMessage = $ErrorMessage } } @@ -532,7 +1014,7 @@ function Invoke-PrepareExtension { When specified, shows what would be done without making changes. .OUTPUTS Hashtable with Success, Version, AgentCount, PromptCount, - InstructionCount, and ErrorMessage properties. + InstructionCount, SkillCount, and ErrorMessage properties. #> [CmdletBinding()] [OutputType([hashtable])] @@ -553,7 +1035,10 @@ function Invoke-PrepareExtension { [string]$ChangelogPath = "", [Parameter(Mandatory = $false)] - [switch]$DryRun + [switch]$DryRun, + + [Parameter(Mandatory = $false)] + [string]$Collection = "" ) # Derive paths @@ -569,6 +1054,14 @@ function Invoke-PrepareExtension { return New-PrepareResult -Success $false -ErrorMessage "Required paths not found: $missingPaths" } + # Load AI artifacts registry if available + $registryPath = Join-Path $GitHubDir "ai-artifacts-registry.json" + $registry = @{} + if (Test-Path $registryPath) { + $registry = Get-RegistryData -RegistryPath $registryPath + Write-Host "Registry loaded: $registryPath" + } + # Read and parse package.json try { $packageJsonContent = Get-Content -Path $PackageJsonPath -Raw @@ -600,9 +1093,41 @@ function Invoke-PrepareExtension { Write-Host "[DRY RUN] No changes will be made" -ForegroundColor Yellow } + # Load collection manifest if specified + $collectionManifest = $null + $collectionArtifactNames = $null + + if ($Collection -and $Collection -ne "") { + $collectionManifest = Get-CollectionManifest -CollectionPath $Collection + Write-Host "Collection: $($collectionManifest.displayName) ($($collectionManifest.id))" + + # Get persona-filtered artifact names + $collectionArtifactNames = Get-CollectionArtifacts -Registry $registry -Collection $collectionManifest -AllowedMaturities $allowedMaturities + + # Resolve handoff dependencies (agents only) + $agentsDir = Join-Path $GitHubDir "agents" + $expandedAgents = Resolve-HandoffDependencies -SeedAgents $collectionArtifactNames.Agents -AgentsDir $agentsDir -AllowedMaturities $allowedMaturities -Registry $registry + $collectionArtifactNames.Agents = $expandedAgents + + # Resolve requires dependencies + $resolvedNames = Resolve-RequiresDependencies -ArtifactNames @{ + agents = $collectionArtifactNames.Agents + prompts = $collectionArtifactNames.Prompts + instructions = $collectionArtifactNames.Instructions + skills = $collectionArtifactNames.Skills + } -Registry $registry -AllowedMaturities $allowedMaturities + + $collectionArtifactNames = @{ + Agents = $resolvedNames.Agents + Prompts = $resolvedNames.Prompts + Instructions = $resolvedNames.Instructions + Skills = $resolvedNames.Skills + } + } + # Discover agents $agentsDir = Join-Path $GitHubDir "agents" - $agentResult = Get-DiscoveredAgents -AgentsDir $agentsDir -AllowedMaturities $allowedMaturities -ExcludedAgents @() + $agentResult = Get-DiscoveredAgents -AgentsDir $agentsDir -AllowedMaturities $allowedMaturities -ExcludedAgents @() -Registry $registry $chatAgents = $agentResult.Agents $excludedAgents = $agentResult.Skipped @@ -614,7 +1139,7 @@ function Invoke-PrepareExtension { # Discover prompts $promptsDir = Join-Path $GitHubDir "prompts" - $promptResult = Get-DiscoveredPrompts -PromptsDir $promptsDir -GitHubDir $GitHubDir -AllowedMaturities $allowedMaturities + $promptResult = Get-DiscoveredPrompts -PromptsDir $promptsDir -GitHubDir $GitHubDir -AllowedMaturities $allowedMaturities -Registry $registry $chatPrompts = $promptResult.Prompts $excludedPrompts = $promptResult.Skipped @@ -626,7 +1151,7 @@ function Invoke-PrepareExtension { # Discover instructions $instructionsDir = Join-Path $GitHubDir "instructions" - $instructionResult = Get-DiscoveredInstructions -InstructionsDir $instructionsDir -GitHubDir $GitHubDir -AllowedMaturities $allowedMaturities + $instructionResult = Get-DiscoveredInstructions -InstructionsDir $instructionsDir -GitHubDir $GitHubDir -AllowedMaturities $allowedMaturities -Registry $registry $chatInstructions = $instructionResult.Instructions $excludedInstructions = $instructionResult.Skipped @@ -636,11 +1161,49 @@ function Invoke-PrepareExtension { Write-Host "Excluded $($excludedInstructions.Count) instruction(s) due to maturity filter" -ForegroundColor Yellow } + # Discover skills + $skillsDir = Join-Path $GitHubDir "skills" + $skillResult = Get-DiscoveredSkills -SkillsDir $skillsDir -AllowedMaturities $allowedMaturities -Registry $registry + $chatSkills = $skillResult.Skills + $excludedSkills = $skillResult.Skipped + + Write-Host "`n--- Chat Skills ---" -ForegroundColor Green + Write-Host "Found $($chatSkills.Count) skill(s) matching criteria" + if ($excludedSkills.Count -gt 0) { + Write-Host "Excluded $($excludedSkills.Count) skill(s) due to maturity filter" -ForegroundColor Yellow + } + + # Apply collection filtering to discovered artifacts + if ($null -ne $collectionArtifactNames) { + $chatAgents = @($chatAgents | Where-Object { $collectionArtifactNames.Agents -contains $_.name }) + $chatPrompts = @($chatPrompts | Where-Object { $collectionArtifactNames.Prompts -contains $_.name }) + $instrBaseNames = @($collectionArtifactNames.Instructions | ForEach-Object { ($_ -split '/')[-1] }) + $chatInstructions = @($chatInstructions | Where-Object { + $instrBaseName = $_.name -replace '-instructions$', '' + $instrBaseNames -contains $instrBaseName + }) + $chatSkills = @($chatSkills | Where-Object { $collectionArtifactNames.Skills -contains $_.name }) + + Write-Host "`n--- Collection Filtering ---" -ForegroundColor Magenta + Write-Host "Agents after filter: $($chatAgents.Count)" + Write-Host "Prompts after filter: $($chatPrompts.Count)" + Write-Host "Instructions after filter: $($chatInstructions.Count)" + Write-Host "Skills after filter: $($chatSkills.Count)" + } + # Update package.json $packageJson = Update-PackageJsonContributes -PackageJson $packageJson ` -ChatAgents $chatAgents ` -ChatPromptFiles $chatPrompts ` - -ChatInstructions $chatInstructions + -ChatInstructions $chatInstructions ` + -ChatSkills $chatSkills + + # Override package.json metadata from collection manifest + if ($null -ne $collectionManifest) { + if ($collectionManifest.ContainsKey('name')) { $packageJson.name = $collectionManifest.name } + if ($collectionManifest.ContainsKey('displayName')) { $packageJson.displayName = $collectionManifest.displayName } + if ($collectionManifest.ContainsKey('description')) { $packageJson.description = $collectionManifest.description } + } # Write updated package.json if (-not $DryRun) { @@ -672,7 +1235,8 @@ function Invoke-PrepareExtension { -Version $version ` -AgentCount $chatAgents.Count ` -PromptCount $chatPrompts.Count ` - -InstructionCount $chatInstructions.Count + -InstructionCount $chatInstructions.Count ` + -SkillCount $chatSkills.Count } #endregion Pure Functions @@ -705,6 +1269,9 @@ if ($MyInvocation.InvocationName -ne '.') { Write-Host "๐Ÿ“ฆ HVE Core Extension Preparer" -ForegroundColor Cyan Write-Host "==============================" -ForegroundColor Cyan Write-Host " Channel: $Channel" -ForegroundColor Cyan + if ($Collection) { + Write-Host " Collection: $Collection" -ForegroundColor Cyan + } Write-Host "" # Call orchestration function @@ -713,7 +1280,8 @@ if ($MyInvocation.InvocationName -ne '.') { -RepoRoot $RepoRoot ` -Channel $Channel ` -ChangelogPath $resolvedChangelogPath ` - -DryRun:$DryRun + -DryRun:$DryRun ` + -Collection $Collection if (-not $result.Success) { throw $result.ErrorMessage @@ -726,6 +1294,7 @@ if ($MyInvocation.InvocationName -ne '.') { Write-Host " Agents: $($result.AgentCount)" Write-Host " Prompts: $($result.PromptCount)" Write-Host " Instructions: $($result.InstructionCount)" + Write-Host " Skills: $($result.SkillCount)" Write-Host " Version: $($result.Version)" exit 0 diff --git a/scripts/linting/schemas/collection.schema.json b/scripts/linting/schemas/collection.schema.json new file mode 100644 index 00000000..2fa9930b --- /dev/null +++ b/scripts/linting/schemas/collection.schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/microsoft/hve-core/schemas/collection.schema.json", + "title": "Collection Manifest Schema", + "description": "Schema for persona-targeted extension collection manifests", + "type": "object", + "required": [ + "$schema", + "id", + "name", + "displayName", + "description", + "personas" + ], + "properties": { + "$schema": { + "type": "string" + }, + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$", + "minLength": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "displayName": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + }, + "personas": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "minItems": 1 + }, + "include": { + "$ref": "#/$defs/artifactGlobFilter" + }, + "exclude": { + "$ref": "#/$defs/artifactGlobFilter" + } + }, + "additionalProperties": false, + "$defs": { + "artifactGlobFilter": { + "type": "object", + "properties": { + "agents": { + "$ref": "#/$defs/globPatternArray" + }, + "prompts": { + "$ref": "#/$defs/globPatternArray" + }, + "instructions": { + "$ref": "#/$defs/globPatternArray" + }, + "skills": { + "$ref": "#/$defs/globPatternArray" + } + }, + "additionalProperties": false + }, + "globPatternArray": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } +} \ No newline at end of file diff --git a/scripts/tests/extension/Package-Extension.Tests.ps1 b/scripts/tests/extension/Package-Extension.Tests.ps1 index 86198ec2..42a92af6 100644 --- a/scripts/tests/extension/Package-Extension.Tests.ps1 +++ b/scripts/tests/extension/Package-Extension.Tests.ps1 @@ -78,6 +78,18 @@ Describe 'Get-ExtensionOutputPath' { $expected = [System.IO.Path]::Combine($script:testDir, 'ext-2.1.0-preview.1.vsix') $result | Should -Be $expected } + + It 'Uses collection ID in filename when specified' { + $result = Get-ExtensionOutputPath -ExtensionDirectory $script:testDir -ExtensionName 'my-extension' -PackageVersion '1.0.0' -CollectionId 'developer' + $expected = [System.IO.Path]::Combine($script:testDir, 'developer-1.0.0.vsix') + $result | Should -Be $expected + } + + It 'Uses extension name when no collection ID' { + $result = Get-ExtensionOutputPath -ExtensionDirectory $script:testDir -ExtensionName 'my-extension' -PackageVersion '1.0.0' -CollectionId '' + $expected = [System.IO.Path]::Combine($script:testDir, 'my-extension-1.0.0.vsix') + $result | Should -Be $expected + } } Describe 'Test-ExtensionManifestValid' { @@ -293,6 +305,7 @@ Describe 'Invoke-PackageExtension' { New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null + New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null New-Item -Path (Join-Path $script:repoRoot 'scripts/dev-tools') -ItemType Directory -Force | Out-Null New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module' @@ -492,6 +505,7 @@ Describe 'Test-PackagingInputsValid' { New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null + New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock' Set-Content -Path (Join-Path $script:extDir 'package.json') -Value '{}' @@ -561,9 +575,9 @@ Describe 'Get-PackagingDirectorySpec' { $script:extDir = Join-Path ([System.IO.Path]::GetTempPath()) 'spec-ext' } - It 'Returns array of 4 directory specifications' { + It 'Returns array of 5 directory specifications' { $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir - $result.Count | Should -Be 4 + $result.Count | Should -Be 5 } It 'Includes .github directory specification' { @@ -595,6 +609,14 @@ Describe 'Get-PackagingDirectorySpec' { $templatesSpec.IsFile | Should -BeFalse } + It 'Includes skills directory specification' { + $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir + $skillsSpec = $result | Where-Object { $_.Source -like '*skills' } + $skillsSpec | Should -Not -BeNullOrEmpty + $skillsSpec.Destination | Should -BeLike '*skills' + $skillsSpec.IsFile | Should -BeFalse + } + It 'Uses correct path joining for source and destination' { $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir foreach ($spec in $result) { @@ -787,6 +809,7 @@ Describe 'CI Integration - Package-Extension' { New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null + New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null New-Item -Path (Join-Path $script:repoRoot 'scripts/dev-tools') -ItemType Directory -Force | Out-Null New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module' @@ -878,6 +901,7 @@ Describe 'CI Integration - Package-Extension' { New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null + New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null New-Item -Path (Join-Path $script:repoRoot 'scripts/dev-tools') -ItemType Directory -Force | Out-Null New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module' diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index b3943748..d7b3c4d9 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -31,45 +31,45 @@ Describe 'Get-FrontmatterData' { Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue } - It 'Extracts description and maturity from frontmatter' { + It 'Extracts description from frontmatter' { $testFile = Join-Path $script:tempDir 'test.md' @' --- description: "Test description" -maturity: preview --- # Content '@ | Set-Content -Path $testFile $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'fallback' $result.description | Should -Be 'Test description' - $result.maturity | Should -Be 'preview' } - It 'Uses fallback description when not in frontmatter' { - $testFile = Join-Path $script:tempDir 'no-desc.md' + It 'Returns hashtable with only description key' { + $testFile = Join-Path $script:tempDir 'desc-only.md' @' --- -maturity: stable +description: "Desc" +maturity: preview --- # Content '@ | Set-Content -Path $testFile - $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'My Fallback' - $result.description | Should -Be 'My Fallback' + $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'fallback' + $result.Keys | Should -Contain 'description' + $result.Keys | Should -Not -Contain 'maturity' } - It 'Defaults maturity to stable when not specified' { - $testFile = Join-Path $script:tempDir 'no-maturity.md' + It 'Uses fallback description when not in frontmatter' { + $testFile = Join-Path $script:tempDir 'no-desc.md' @' --- -description: "Desc" +applyTo: "**" --- # Content '@ | Set-Content -Path $testFile - $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'fallback' - $result.maturity | Should -Be 'stable' + $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'My Fallback' + $result.description | Should -Be 'My Fallback' } } @@ -122,16 +122,21 @@ Describe 'Get-DiscoveredAgents' { @' --- description: "Stable agent" -maturity: stable --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'stable.agent.md') @' --- description: "Preview agent" -maturity: preview --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'preview.agent.md') + + $script:mockRegistry = @{ + agents = @{ + 'stable' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + 'preview' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() } + } + } } AfterAll { @@ -139,19 +144,19 @@ maturity: preview } It 'Discovers agents matching allowed maturities' { - $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @() + $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @() -Registry $script:mockRegistry $result.DirectoryExists | Should -BeTrue $result.Agents.Count | Should -Be 2 } It 'Filters agents by maturity' { - $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -ExcludedAgents @() + $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -ExcludedAgents @() -Registry $script:mockRegistry $result.Agents.Count | Should -Be 1 $result.Skipped.Count | Should -Be 1 } It 'Excludes specified agents' { - $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @('stable') + $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @('stable') -Registry $script:mockRegistry $result.Agents.Count | Should -Be 1 } @@ -174,7 +179,6 @@ Describe 'Get-DiscoveredPrompts' { @' --- description: "Test prompt" -maturity: stable --- '@ | Set-Content -Path (Join-Path $script:promptsDir 'test.prompt.md') } @@ -208,7 +212,6 @@ Describe 'Get-DiscoveredInstructions' { --- description: "Test instruction" applyTo: "**/*.ps1" -maturity: stable --- '@ | Set-Content -Path (Join-Path $script:instrDir 'test.instructions.md') } @@ -230,6 +233,382 @@ maturity: stable } } +Describe 'Get-RegistryData' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Loads registry from valid path' { + $registryFile = Join-Path $script:tempDir 'registry.json' + @{ agents = @{ 'test' = @{ maturity = 'stable' } } } | ConvertTo-Json -Depth 5 | Set-Content -Path $registryFile + + $result = Get-RegistryData -RegistryPath $registryFile + $result | Should -Not -BeNullOrEmpty + } + + It 'Throws when path does not exist' { + $nonexistent = Join-Path $script:tempDir 'nonexistent.json' + { Get-RegistryData -RegistryPath $nonexistent } | Should -Throw '*not found*' + } + + It 'Returns hashtable with expected keys' { + $registryFile = Join-Path $script:tempDir 'registry2.json' + @{ + agents = @{ 'a' = @{ maturity = 'stable' } } + prompts = @{ 'p' = @{ maturity = 'stable' } } + } | ConvertTo-Json -Depth 5 | Set-Content -Path $registryFile + + $result = Get-RegistryData -RegistryPath $registryFile + $result.Keys | Should -Contain 'agents' + $result.Keys | Should -Contain 'prompts' + } +} + +Describe 'Get-DiscoveredSkills' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + $script:skillsDir = Join-Path $script:tempDir 'skills' + New-Item -ItemType Directory -Path $script:skillsDir -Force | Out-Null + + # Create test skill + $skillDir = Join-Path $script:skillsDir 'test-skill' + New-Item -ItemType Directory -Path $skillDir -Force | Out-Null + @' +--- +name: test-skill +description: "Test skill" +--- +# Skill +'@ | Set-Content -Path (Join-Path $skillDir 'SKILL.md') + + # Create empty skill directory (no SKILL.md) + $emptySkillDir = Join-Path $script:skillsDir 'empty-skill' + New-Item -ItemType Directory -Path $emptySkillDir -Force | Out-Null + + $script:mockRegistry = @{ + skills = @{ + 'test-skill' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + } + } + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Discovers skills in directory' { + $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry + $result.DirectoryExists | Should -BeTrue + $result.Skills.Count | Should -Be 1 + $result.Skills[0].name | Should -Be 'test-skill' + } + + It 'Returns empty when directory does not exist' { + $nonexistent = Join-Path $script:tempDir 'nonexistent-skills' + $result = Get-DiscoveredSkills -SkillsDir $nonexistent -AllowedMaturities @('stable') + $result.DirectoryExists | Should -BeFalse + $result.Skills | Should -BeNullOrEmpty + } + + It 'Filters skills by maturity from registry' { + $previewRegistry = @{ + skills = @{ + 'test-skill' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() } + } + } + $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable') -Registry $previewRegistry + $result.Skills.Count | Should -Be 0 + $result.Skipped.Count | Should -BeGreaterThan 0 + } + + It 'Skips directories without SKILL.md' { + $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry + $skippedNames = $result.Skipped | ForEach-Object { $_.Name } + $skippedNames | Should -Contain 'empty-skill' + } +} + +Describe 'Get-CollectionManifest' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Loads collection manifest from valid path' { + $manifestFile = Join-Path $script:tempDir 'test.collection.json' + @{ + '$schema' = '../schemas/collection.schema.json' + id = 'test' + name = 'test-ext' + displayName = 'Test Extension' + description = 'Test' + personas = @('hve-core-all') + } | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestFile + + $result = Get-CollectionManifest -CollectionPath $manifestFile + $result | Should -Not -BeNullOrEmpty + $result.id | Should -Be 'test' + } + + It 'Throws when path does not exist' { + $nonexistent = Join-Path $script:tempDir 'nonexistent.json' + { Get-CollectionManifest -CollectionPath $nonexistent } | Should -Throw '*not found*' + } + + It 'Returns hashtable with expected keys' { + $manifestFile = Join-Path $script:tempDir 'keys.collection.json' + @{ + '$schema' = '../schemas/collection.schema.json' + id = 'keys' + name = 'keys-ext' + displayName = 'Keys' + description = 'Keys test' + personas = @('developer') + } | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestFile + + $result = Get-CollectionManifest -CollectionPath $manifestFile + $result.Keys | Should -Contain 'id' + $result.Keys | Should -Contain 'name' + $result.Keys | Should -Contain 'personas' + } +} + +Describe 'Test-GlobMatch' { + It 'Returns true for matching wildcard pattern' { + $result = Test-GlobMatch -Name 'rpi-agent' -Patterns @('rpi-*') + $result | Should -BeTrue + } + + It 'Returns false for non-matching pattern' { + $result = Test-GlobMatch -Name 'memory' -Patterns @('rpi-*') + $result | Should -BeFalse + } + + It 'Matches against multiple patterns' { + $result = Test-GlobMatch -Name 'memory' -Patterns @('rpi-*', 'mem*') + $result | Should -BeTrue + } + + It 'Handles exact name match' { + $result = Test-GlobMatch -Name 'memory' -Patterns @('memory') + $result | Should -BeTrue + } +} + +Describe 'Get-CollectionArtifacts' { + BeforeAll { + $script:registry = @{ + agents = @{ + 'dev-agent' = @{ maturity = 'stable'; personas = @('developer'); tags = @() } + 'all-agent' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + 'preview-dev' = @{ maturity = 'preview'; personas = @('developer'); tags = @() } + } + prompts = @{ + 'dev-prompt' = @{ maturity = 'stable'; personas = @('developer'); tags = @() } + } + instructions = @{} + skills = @{} + } + } + + It 'Filters by persona' { + $collection = @{ personas = @('developer') } + $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable', 'preview') + $result.Agents | Should -Contain 'dev-agent' + $result.Agents | Should -Not -Contain 'all-agent' + } + + It 'Applies include patterns' { + $collection = @{ + personas = @('developer') + include = @{ agents = @('dev-*') } + } + $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable', 'preview') + $result.Agents | Should -Contain 'dev-agent' + } + + It 'Applies exclude patterns' { + $collection = @{ + personas = @('developer') + exclude = @{ agents = @('preview-*') } + } + $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable', 'preview') + $result.Agents | Should -Contain 'dev-agent' + $result.Agents | Should -Not -Contain 'preview-dev' + } + + It 'Respects maturity filter' { + $collection = @{ personas = @('developer') } + $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable') + $result.Agents | Should -Contain 'dev-agent' + $result.Agents | Should -Not -Contain 'preview-dev' + } +} + +Describe 'Resolve-HandoffDependencies' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + $script:agentsDir = Join-Path $script:tempDir 'agents' + New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null + + # Agent with no handoffs + @' +--- +description: "Solo agent" +--- +'@ | Set-Content -Path (Join-Path $script:agentsDir 'solo.agent.md') + + # Agent with single handoff + @' +--- +description: "Parent agent" +handoffs: + - child +--- +'@ | Set-Content -Path (Join-Path $script:agentsDir 'parent.agent.md') + + @' +--- +description: "Child agent" +--- +'@ | Set-Content -Path (Join-Path $script:agentsDir 'child.agent.md') + + # Self-referential agent + @' +--- +description: "Self agent" +handoffs: + - self-ref +--- +'@ | Set-Content -Path (Join-Path $script:agentsDir 'self-ref.agent.md') + + # Circular chain + @' +--- +description: "Chain A" +handoffs: + - chain-b +--- +'@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-a.agent.md') + + @' +--- +description: "Chain B" +handoffs: + - chain-a +--- +'@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-b.agent.md') + + $script:mockRegistry = @{ + agents = @{ + 'solo' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + 'parent' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + 'child' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + 'self-ref' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + 'chain-a' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + 'chain-b' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + } + } + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Returns seed agents when no handoffs' { + $result = Resolve-HandoffDependencies -SeedAgents @('solo') -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry + $result | Should -Contain 'solo' + $result.Count | Should -Be 1 + } + + It 'Resolves single-level handoff' { + $result = Resolve-HandoffDependencies -SeedAgents @('parent') -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry + $result | Should -Contain 'parent' + $result | Should -Contain 'child' + } + + It 'Handles self-referential handoffs' { + $result = Resolve-HandoffDependencies -SeedAgents @('self-ref') -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry + $result | Should -Contain 'self-ref' + $result.Count | Should -Be 1 + } + + It 'Handles circular handoff chains' { + $result = Resolve-HandoffDependencies -SeedAgents @('chain-a') -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry + $result | Should -Contain 'chain-a' + $result | Should -Contain 'chain-b' + $result.Count | Should -Be 2 + } +} + +Describe 'Resolve-RequiresDependencies' { + It 'Resolves agent requires to include dependent prompts' { + $registry = @{ + agents = @{ + 'main' = @{ + maturity = 'stable' + personas = @('hve-core-all') + requires = @{ prompts = @('dep-prompt') } + } + } + prompts = @{ + 'dep-prompt' = @{ maturity = 'stable'; personas = @('hve-core-all') } + } + } + $result = Resolve-RequiresDependencies -ArtifactNames @{ agents = @('main') } -Registry $registry -AllowedMaturities @('stable') + $result.Prompts | Should -Contain 'dep-prompt' + } + + It 'Resolves transitive agent dependencies' { + $registry = @{ + agents = @{ + 'top' = @{ + maturity = 'stable' + personas = @('hve-core-all') + requires = @{ agents = @('mid') } + } + 'mid' = @{ + maturity = 'stable' + personas = @('hve-core-all') + requires = @{ prompts = @('leaf-prompt') } + } + } + prompts = @{ + 'leaf-prompt' = @{ maturity = 'stable'; personas = @('hve-core-all') } + } + } + $result = Resolve-RequiresDependencies -ArtifactNames @{ agents = @('top') } -Registry $registry -AllowedMaturities @('stable') + $result.Agents | Should -Contain 'mid' + $result.Prompts | Should -Contain 'leaf-prompt' + } + + It 'Respects maturity filter on dependencies' { + $registry = @{ + agents = @{ + 'main' = @{ + maturity = 'stable' + personas = @('hve-core-all') + requires = @{ prompts = @('exp-prompt') } + } + } + prompts = @{ + 'exp-prompt' = @{ maturity = 'experimental'; personas = @('hve-core-all') } + } + } + $result = Resolve-RequiresDependencies -ArtifactNames @{ agents = @('main') } -Registry $registry -AllowedMaturities @('stable') + $result.Prompts | Should -Not -Contain 'exp-prompt' + } +} + Describe 'Update-PackageJsonContributes' { It 'Updates contributes section with chat participants' { $packageJson = [PSCustomObject]@{ @@ -246,7 +625,7 @@ Describe 'Update-PackageJsonContributes' { @{ name = 'instr1'; description = 'Instr desc' } ) - $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents $agents -ChatPromptFiles $prompts -ChatInstructions $instructions + $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents $agents -ChatPromptFiles $prompts -ChatInstructions $instructions -ChatSkills @() $result.contributes | Should -Not -BeNullOrEmpty } @@ -256,18 +635,19 @@ Describe 'Update-PackageJsonContributes' { contributes = [PSCustomObject]@{} } - $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents @() -ChatPromptFiles @() -ChatInstructions @() + $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents @() -ChatPromptFiles @() -ChatInstructions @() -ChatSkills @() $result | Should -Not -BeNullOrEmpty } } Describe 'New-PrepareResult' { It 'Creates success result with counts' { - $result = New-PrepareResult -Success $true -AgentCount 5 -PromptCount 10 -InstructionCount 15 -Version '1.0.0' + $result = New-PrepareResult -Success $true -AgentCount 5 -PromptCount 10 -InstructionCount 15 -SkillCount 3 -Version '1.0.0' $result.Success | Should -BeTrue $result.AgentCount | Should -Be 5 $result.PromptCount | Should -Be 10 $result.InstructionCount | Should -Be 15 + $result.SkillCount | Should -Be 3 $result.Version | Should -Be '1.0.0' $result.ErrorMessage | Should -BeNullOrEmpty } @@ -287,6 +667,7 @@ Describe 'New-PrepareResult' { $result.Keys | Should -Contain 'AgentCount' $result.Keys | Should -Contain 'PromptCount' $result.Keys | Should -Contain 'InstructionCount' + $result.Keys | Should -Contain 'SkillCount' $result.Keys | Should -Contain 'Version' $result.Keys | Should -Contain 'ErrorMessage' } @@ -321,7 +702,6 @@ Describe 'Invoke-PrepareExtension' { @' --- description: "Test agent" -maturity: stable --- # Agent '@ | Set-Content -Path (Join-Path $script:agentsDir 'test.agent.md') @@ -330,7 +710,6 @@ maturity: stable @' --- description: "Test prompt" -maturity: stable --- # Prompt '@ | Set-Content -Path (Join-Path $script:promptsDir 'test.prompt.md') @@ -340,10 +719,26 @@ maturity: stable --- description: "Test instruction" applyTo: "**/*.ps1" -maturity: stable --- # Instruction '@ | Set-Content -Path (Join-Path $script:instrDir 'test.instructions.md') + + # Create mock registry for all Invoke-PrepareExtension tests + $registryContent = @{ + version = "1.0" + personas = @{ definitions = @{ 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } } } + agents = @{ + 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + } + prompts = @{ + 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + } + instructions = @{ + 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + } + skills = @{} + } + $registryContent | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:ghDir 'ai-artifacts-registry.json') } AfterAll { @@ -380,10 +775,27 @@ maturity: stable @' --- description: "Preview agent" -maturity: preview --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'preview.agent.md') + # Update registry with preview agent + $registryContent = @{ + version = "1.0" + personas = @{ definitions = @{ 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } } } + agents = @{ + 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + 'preview' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() } + } + prompts = @{ + 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + } + instructions = @{ + 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + } + skills = @{} + } + $registryContent | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:ghDir 'ai-artifacts-registry.json') + $stableResult = Invoke-PrepareExtension ` -ExtensionDirectory $script:extDir ` -RepoRoot $script:tempDir ` @@ -404,7 +816,6 @@ maturity: preview @' --- description: "Experimental prompt" -maturity: experimental --- '@ | Set-Content -Path (Join-Path $script:promptsDir 'experimental.prompt.md') @@ -413,10 +824,29 @@ maturity: experimental --- description: "Preview instruction" applyTo: "**/*.js" -maturity: preview --- '@ | Set-Content -Path (Join-Path $script:instrDir 'preview.instructions.md') + # Update registry with all artifacts + $registryContent = @{ + version = "1.0" + personas = @{ definitions = @{ 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } } } + agents = @{ + 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + 'preview' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() } + } + prompts = @{ + 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + 'experimental' = @{ maturity = 'experimental'; personas = @('hve-core-all'); tags = @() } + } + instructions = @{ + 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } + 'preview' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() } + } + skills = @{} + } + $registryContent | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:ghDir 'ai-artifacts-registry.json') + $stableResult = Invoke-PrepareExtension ` -ExtensionDirectory $script:extDir ` -RepoRoot $script:tempDir ` @@ -429,7 +859,6 @@ maturity: preview -Channel 'PreRelease' ` -DryRun - # Stable should have fewer prompts and instructions than PreRelease $preReleaseResult.PromptCount | Should -BeGreaterThan $stableResult.PromptCount $preReleaseResult.InstructionCount | Should -BeGreaterThan $stableResult.InstructionCount } From 0fcf059646c80f7898bd4633f8bc596101998570 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 16:26:26 +0100 Subject: [PATCH 08/62] refactor(extension): update Copy-CollectionArtifacts function to remove unused code and clarify parameter usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - remove redundant copilot-instructions.md copy logic - clarify PrepareResult parameter for future use ๐Ÿ”ง - Generated by Copilot --- scripts/extension/Package-Extension.ps1 | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index 1ac84113..fb15fb20 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -473,15 +473,16 @@ function Copy-CollectionArtifacts { .DESCRIPTION Reads the prepared package.json to determine which artifacts were selected by collection filtering, then copies only those files instead of the entire - .github directory. Always includes copilot-instructions.md. + .github directory. .PARAMETER RepoRoot Absolute path to the repository root. .PARAMETER ExtensionDirectory Absolute path to the extension directory. .PARAMETER PrepareResult - Result hashtable from Invoke-PrepareExtension (unused directly, kept for API consistency). + Result hashtable from Invoke-PrepareExtension. Reserved for future collection metadata handling. #> [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'PrepareResult', Justification = 'Reserved for future collection metadata handling')] param( [Parameter(Mandatory = $true)] [string]$RepoRoot, @@ -543,15 +544,6 @@ function Copy-CollectionArtifacts { } } } - - # Always copy copilot-instructions.md (needed by all collections) - $copilotInstr = Join-Path $RepoRoot ".github/copilot-instructions.md" - $copilotDest = Join-Path $ExtensionDirectory ".github/copilot-instructions.md" - if (Test-Path $copilotInstr) { - $copilotDestDir = Split-Path $copilotDest -Parent - New-Item -Path $copilotDestDir -ItemType Directory -Force | Out-Null - Copy-Item -Path $copilotInstr -Destination $copilotDest -Force - } } function Invoke-VsceCommand { @@ -800,13 +792,6 @@ function Invoke-PackageExtension { $versionWasModified = $true } - # Extract collection ID for output naming - $collectionId = $null - if ($Collection -and $Collection -ne "") { - $collectionContent = Get-Content -Path $Collection -Raw | ConvertFrom-Json - $collectionId = $collectionContent.id - } - # Handle changelog if provided if ($ChangelogPath -and $ChangelogPath -ne "") { Write-Host "" From ae56f2d34abf80376d05a0812d39b8d0f24bd80a Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 17:00:05 +0100 Subject: [PATCH 09/62] refactor(extension): remove unused collection skills from packaging directory specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(tests): update agent handoff definitions to use object format for clarity style(linting): remove maturity enum validation from artifact registry description ๐Ÿ”ง - Generated by Copilot --- scripts/extension/Package-Extension.ps1 | 5 ---- scripts/extension/Prepare-Extension.ps1 | 24 ++++++++++++++----- scripts/linting/Validate-ArtifactRegistry.ps1 | 1 - .../extension/Prepare-Extension.Tests.ps1 | 19 +++++++++------ 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index fb15fb20..6bbe0a24 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -439,11 +439,6 @@ function Get-PackagingDirectorySpec { Destination = Join-Path $ExtensionDirectory ".github" IsFile = $false }, - @{ - Source = Join-Path $RepoRoot ".github/skills" - Destination = Join-Path $ExtensionDirectory ".github/skills" - IsFile = $false - }, @{ Source = Join-Path $RepoRoot "scripts/dev-tools" Destination = Join-Path $ExtensionDirectory "scripts/dev-tools" diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index eb4343e5..3bb6f0c8 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -234,11 +234,16 @@ function Get-CollectionArtifacts { $entry = $Registry[$type][$name] # Persona filter: artifact must belong to at least one collection persona + # Empty personas array means universal (all personas) $personaMatch = $false - foreach ($persona in $entry.personas) { - if ($collectionPersonas -contains $persona) { - $personaMatch = $true - break + if (@($entry.personas).Count -eq 0) { + $personaMatch = $true + } else { + foreach ($persona in $entry.personas) { + if ($collectionPersonas -contains $persona) { + $personaMatch = $true + break + } } } if (-not $personaMatch) { continue } @@ -332,8 +337,15 @@ function Resolve-HandoffDependencies { $data = ConvertFrom-Yaml -Yaml $yamlContent if ($data.ContainsKey('handoffs') -and $data.handoffs -is [System.Collections.IEnumerable] -and $data.handoffs -isnot [string]) { foreach ($handoff in $data.handoffs) { - if ($handoff -is [string] -and $visited.Add($handoff)) { - $queue.Enqueue($handoff) + # Handle both string format and object format (with 'agent' field) + $targetAgent = $null + if ($handoff -is [string]) { + $targetAgent = $handoff + } elseif ($handoff -is [hashtable] -and $handoff.ContainsKey('agent')) { + $targetAgent = $handoff.agent + } + if ($targetAgent -and $visited.Add($targetAgent)) { + $queue.Enqueue($targetAgent) } } } diff --git a/scripts/linting/Validate-ArtifactRegistry.ps1 b/scripts/linting/Validate-ArtifactRegistry.ps1 index 8b934909..e8b044a1 100644 --- a/scripts/linting/Validate-ArtifactRegistry.ps1 +++ b/scripts/linting/Validate-ArtifactRegistry.ps1 @@ -15,7 +15,6 @@ .DESCRIPTION Validates the `.github/ai-artifacts-registry.json` file by checking: - JSON structure and required fields - - Maturity enum values - Persona ID format and reference validity - Artifact file existence on disk - Dependency reference validity diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index d7b3c4d9..34e2b8d5 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -467,12 +467,14 @@ description: "Solo agent" --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'solo.agent.md') - # Agent with single handoff + # Agent with single handoff (object format matching real agents) @' --- description: "Parent agent" handoffs: - - child + - label: "Go to child" + agent: child + prompt: Continue --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'parent.agent.md') @@ -482,21 +484,23 @@ description: "Child agent" --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'child.agent.md') - # Self-referential agent + # Self-referential agent (object format) @' --- description: "Self agent" handoffs: - - self-ref + - label: "Self" + agent: self-ref --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'self-ref.agent.md') - # Circular chain + # Circular chain (object format) @' --- description: "Chain A" handoffs: - - chain-b + - label: "To B" + agent: chain-b --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-a.agent.md') @@ -504,7 +508,8 @@ handoffs: --- description: "Chain B" handoffs: - - chain-a + - label: "To A" + agent: chain-a --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-b.agent.md') From 278521aece8259eb7949f1e30e1262f6ca22a7c9 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 17:19:26 +0100 Subject: [PATCH 10/62] feat(docs): enhance AI artifacts documentation with registry and persona guidelines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add artifact registry section to multiple documentation files - outline persona selection criteria for agents, prompts, instructions, and skills - introduce collection-based packaging and installation methods Refs #435 ๐Ÿ“š - Generated by Copilot --- docs/architecture/ai-artifacts.md | 119 ++++++++++++++++- docs/contributing/ai-artifacts-common.md | 159 +++++++++++++++++++++++ docs/contributing/custom-agents.md | 50 +++++++ docs/contributing/instructions.md | 54 ++++++++ docs/contributing/prompts.md | 47 +++++++ docs/contributing/skills.md | 41 ++++++ docs/getting-started/install.md | 26 ++++ extension/PACKAGING.md | 126 ++++++++++++++++++ 8 files changed, 621 insertions(+), 1 deletion(-) diff --git a/docs/architecture/ai-artifacts.md b/docs/architecture/ai-artifacts.md index 97b53f27..ab8d6422 100644 --- a/docs/architecture/ai-artifacts.md +++ b/docs/architecture/ai-artifacts.md @@ -200,6 +200,112 @@ Skills provide self-contained utilities through the `SKILL.md` file: Copilot discovers skills automatically when their description matches the current task context. Skills can also be referenced explicitly by name. The skill's `SKILL.md` documents prerequisites, parameters, and usage patterns. Cross-platform scripts ensure consistent behavior across operating systems. +## Artifact Registry + +The artifact registry (`.github/ai-artifacts-registry.json`) serves as the central metadata store for all AI artifacts. It enables persona-based distribution, maturity filtering, and dependency resolution without polluting individual artifact frontmatter. + +### Registry Architecture + +```text +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ AI Artifacts Registry โ”‚ +โ”‚ .github/ai-artifacts-registry.json โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Agents โ”‚ Prompts โ”‚ Instructions โ”‚ โ”‚ +โ”‚ โ”‚ - maturity โ”‚ - maturity โ”‚ - maturity โ”‚ โ”‚ +โ”‚ โ”‚ - personas[] โ”‚ - personas[] โ”‚ - personas[] โ”‚ โ”‚ +โ”‚ โ”‚ - tags[] โ”‚ - tags[] โ”‚ - tags[] โ”‚ โ”‚ +โ”‚ โ”‚ - requires{} โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Build System โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Collection โ”‚ โ”‚ Prepare- โ”‚ โ”‚ +โ”‚ โ”‚ Manifests โ”‚โ”€โ”€โ”€โ–ถโ”‚ Extension.ps1 โ”‚ โ”‚ +โ”‚ โ”‚ *.collection.json โ”‚ -Collection โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Registry Entry Structure + +Each artifact entry contains metadata for filtering and dependency resolution: + +```json +{ + "artifact-name": { + "maturity": "stable", + "personas": ["hve-core-all", "developer"], + "tags": ["rpi", "workflow"], + "requires": { + "agents": ["dependency-agent"], + "prompts": ["dependency-prompt"], + "instructions": ["dependency-instructions"], + "skills": [] + } + } +} +``` + +| Field | Purpose | +|------------|------------------------------------------------------| +| `maturity` | Controls extension channel inclusion | +| `personas` | Determines collection membership | +| `tags` | Categorization for organization and discovery | +| `requires` | Declares dependencies for complete installation | + +### Persona Model + +Personas represent user roles that consume artifacts. The registry defines these personas: + +| Persona | Identifier | Target Users | +|---------------|----------------|----------------------| +| **All** | `hve-core-all` | Universal inclusion | +| **Developer** | `developer` | Software engineers | + +Artifacts assigned to `hve-core-all` appear in the full collection and may also include role-specific personas for targeted distribution. + +### Collection Build System + +Collections define persona-filtered artifact packages. Each collection manifest specifies which personas to include: + +```json +{ + "id": "developer", + "name": "hve-developer", + "displayName": "HVE Core - Developer Edition", + "description": "AI-powered coding agents curated for software engineers", + "personas": ["developer"] +} +``` + +The build system resolves collections by: + +1. Reading the collection manifest to identify target personas +2. Filtering registry entries by persona membership +3. Including the `hve-core-all` persona artifacts as the base +4. Adding persona-specific artifacts +5. Resolving dependencies for included artifacts + +### Dependency Resolution + +Agents may declare dependencies on other artifacts through the `requires` field. The dependency resolver ensures complete artifact graphs are installed: + +```mermaid +graph TD + A[rpi-agent] --> B[task-researcher] + A --> C[task-planner] + A --> D[task-implementor] + A --> E[task-reviewer] + A --> F[checkpoint.prompt] + A --> G[rpi.prompt] +``` + +When installing `rpi-agent`, all dependent agents and prompts are automatically included regardless of persona filter. + ## Extension Integration The VS Code extension discovers and activates AI artifacts through contribution points. @@ -213,7 +319,7 @@ The extension scans these directories at startup: * `.github/instructions/` for technology standards * `.github/skills/` for utility packages -Each artifact's `maturity:` field controls channel inclusion: +Artifact inclusion is controlled by the registry: | Maturity | Stable Channel | Pre-release Channel | |----------------|----------------|---------------------| @@ -222,6 +328,17 @@ Each artifact's `maturity:` field controls channel inclusion: | `experimental` | Excluded | Included | | `deprecated` | Excluded | Excluded | +### Collection Packages + +Multiple extension packages can be built from the same codebase: + +| Collection | Extension ID | Contents | +|------------|------------------------------------|-----------------------------| +| Full | `ise-hve-essentials.hve-core` | All stable artifacts | +| Developer | `ise-hve-essentials.hve-developer` | Developer-focused artifacts | + +Users install the collection matching their role for a curated experience. + ### Activation Context Instructions activate based on the current file's path matching `applyTo:` patterns. Prompts and agents activate through explicit user invocation. Skills activate when agents or users request their utilities. diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 32f418bb..5ef0d902 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -85,6 +85,165 @@ All AI artifacts (agents, instructions, prompts) **MUST** target the **latest av 3. **Performance**: Latest models provide superior reasoning, accuracy, and efficiency 4. **Future-proofing**: Older models will be deprecated and removed from service +## Artifact Registry + +All AI artifacts are registered in `.github/ai-artifacts-registry.json`, a centralized metadata file that controls distribution, persona filtering, and dependency resolution without polluting individual artifact frontmatter. + +### Registry Purpose + +The registry serves three primary functions: + +1. **Maturity filtering**: Controls which extension channel (stable vs pre-release) includes each artifact +2. **Persona filtering**: Determines which artifacts appear in role-specific collection packages +3. **Dependency resolution**: Declares explicit dependencies between artifacts for complete installation + +### Registry Structure + +The registry contains four top-level sections: + +```json +{ + "$schema": "../scripts/linting/schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { /* persona definitions */ }, + "agents": { /* agent entries */ }, + "prompts": { /* prompt entries */ }, + "instructions": { /* instruction entries */ }, + "skills": { /* skill entries */ } +} +``` + +### Artifact Entry Format + +Each artifact entry follows this structure: + +```json +"artifact-name": { + "maturity": "stable", + "personas": ["hve-core-all", "developer"], + "tags": ["rpi", "planning"], + "requires": { + "agents": [], + "prompts": ["task-plan"], + "instructions": ["commit-message"], + "skills": [] + } +} +``` + +| Field | Required | Description | +|------------|----------|------------------------------------------------------| +| `maturity` | Yes | Release readiness level | +| `personas` | Yes | Array of persona identifiers for collection building | +| `tags` | Yes | Categorization tags for organization and search | +| `requires` | No | Dependency declarations for complete installation | + +### Adding Artifacts to the Registry + +When contributing a new artifact: + +1. Create the artifact file in the appropriate directory +2. Add an entry to the registry under the correct section (`agents`, `prompts`, `instructions`, or `skills`) +3. Set appropriate `maturity`, `personas`, and `tags` values +4. Declare any dependencies in `requires` if the artifact depends on other artifacts +5. Run `npm run lint:registry` to validate the registry entry + +## Persona Taxonomy + +Personas represent user roles that consume HVE-Core artifacts. The persona system enables role-specific artifact collections without fragmenting the codebase. + +### Defined Personas + +| Persona | Identifier | Description | +|---------------|----------------|---------------------------------| +| **All** | `hve-core-all` | Full release with all artifacts | +| **Developer** | `developer` | Software engineers writing code | + +### Persona Assignment Guidelines + +When assigning personas to artifacts: + +* **Universal artifacts** should include `hve-core-all` plus any role-specific personas that particularly benefit +* **Role-specific artifacts** should include only the relevant personas (omit `hve-core-all` for highly specialized artifacts) +* **Cross-cutting tools** like RPI workflow artifacts (`task-researcher`, `task-planner`) should include multiple relevant personas + +**Example persona assignments:** + +```json +// Universal - available in all collections +"markdown": { + "personas": ["hve-core-all", "developer"] +} + +// Developer-focused - targeted distribution +"csharp/csharp": { + "personas": ["hve-core-all", "developer"] +} + +// Core workflow - broadly applicable +"rpi-agent": { + "personas": ["hve-core-all", "developer"] +} +``` + +### Selecting Personas for New Artifacts + +Answer these questions when determining persona assignments: + +1. **Who is the primary user?** Identify the main role that benefits from this artifact +2. **Who else benefits?** Consider secondary roles that may find value +3. **Is it foundational?** Core workflow artifacts should include multiple personas +4. **Is it specialized?** Domain-specific artifacts may target fewer personas + +When in doubt, include `hve-core-all` to ensure the artifact appears in the full collection while still enabling targeted distribution. + +## Dependency Declarations + +Some artifacts require other artifacts to function correctly. The `requires` field in registry entries declares these dependencies explicitly. + +### Dependency Types + +| Type | Purpose | +|----------------|----------------------------------------------------------| +| `agents` | Agents this artifact delegates to or hands off to | +| `prompts` | Prompts this artifact invokes or references | +| `instructions` | Instructions this artifact relies on for code generation | +| `skills` | Skills this artifact executes for specialized tasks | + +### Declaring Dependencies + +Add the `requires` field to artifacts that depend on others: + +```json +"rpi-agent": { + "maturity": "stable", + "personas": ["hve-core-all"], + "tags": ["rpi", "orchestration"], + "requires": { + "agents": ["task-researcher", "task-planner", "task-implementor", "task-reviewer"], + "prompts": ["task-research", "task-plan", "task-implement", "task-review"], + "instructions": [], + "skills": [] + } +} +``` + +### Dependency Resolution + +When installing artifacts via clone methods, the installer agent resolves dependencies: + +1. User selects artifacts to install (by persona or explicit selection) +2. Installer reads the registry and builds dependency graph +3. Required artifacts are automatically included regardless of persona filter +4. Circular dependencies are detected and reported as validation errors + +### Dependency Best Practices + +* **Declare all runtime dependencies**: List every artifact your artifact references +* **Prefer explicit over implicit**: Document dependencies even if currently co-located +* **Keep dependencies minimal**: Avoid unnecessary coupling between artifacts +* **Test with minimal installs**: Verify your artifact works with only declared dependencies + ## Maturity Field Requirements Maturity is defined in `.github/ai-artifacts-registry.json` and MUST NOT appear in artifact frontmatter. diff --git a/docs/contributing/custom-agents.md b/docs/contributing/custom-agents.md index 898c37fa..3d85aa75 100644 --- a/docs/contributing/custom-agents.md +++ b/docs/contributing/custom-agents.md @@ -200,6 +200,56 @@ author: 'microsoft/hve-core' --- ``` +## Registry Entry Requirements + +All agents must have a corresponding entry in `.github/ai-artifacts-registry.json`. This entry controls distribution, persona filtering, and dependency resolution. + +### Adding Your Agent to the Registry + +After creating your agent file, add an entry to the `agents` section of the registry: + +```json +"my-new-agent": { + "maturity": "stable", + "personas": ["hve-core-all", "developer"], + "tags": ["workflow", "automation"], + "requires": { + "agents": [], + "prompts": ["related-prompt"], + "instructions": ["relevant-instructions"], + "skills": [] + } +} +``` + +### Selecting Personas for Agents + +Choose personas based on who benefits most from your agent: + +| Agent Type | Recommended Personas | +|---------------------------|---------------------------------------------| +| Task workflow agents | `hve-core-all`, `developer`, `tpm` | +| Architecture agents | `hve-core-all`, `architect`, `devops` | +| Documentation agents | `hve-core-all`, `technical-writer` | +| Data science agents | `hve-core-all`, `developer` | +| ADO/work item agents | `hve-core-all`, `tpm`, `devops` | +| Code review agents | `hve-core-all`, `developer` | + +### Declaring Agent Dependencies + +If your agent delegates to other agents, invokes prompts, or generates code that follows specific instructions, declare these in the `requires` field: + +```json +"requires": { + "agents": ["task-planner"], // Agents this agent hands off to + "prompts": ["task-plan"], // Prompts this agent invokes + "instructions": ["python-script"], // Instructions for generated code + "skills": [] // Skills this agent executes +} +``` + +For complete registry documentation, see [AI Artifacts Common Standards - Artifact Registry](ai-artifacts-common.md#artifact-registry). + ### MCP Tool Dependencies When agents reference MCP tools in their `tools:` frontmatter or body content, document the dependencies clearly. diff --git a/docs/contributing/instructions.md b/docs/contributing/instructions.md index 7b6ba87d..d693aa0c 100644 --- a/docs/contributing/instructions.md +++ b/docs/contributing/instructions.md @@ -114,6 +114,60 @@ lastUpdated: '2025-11-19' --- ``` +## Registry Entry Requirements + +All instructions must have a corresponding entry in `.github/ai-artifacts-registry.json`. This entry controls distribution and persona filtering. + +### Adding Your Instructions to the Registry + +After creating your instructions file, add an entry to the `instructions` section of the registry: + +```json +"my-language": { + "maturity": "stable", + "personas": ["hve-core-all", "developer"], + "tags": ["language"] +} +``` + +For instructions in subdirectories, use the path format: + +```json +"subdirectory/my-instructions": { + "maturity": "stable", + "personas": ["hve-core-all"], + "tags": ["category"] +} +``` + +### Selecting Personas for Instructions + +Choose personas based on who uses the technology or pattern: + +| Instruction Type | Recommended Personas | +|---------------------------|---------------------------------------------------| +| Language standards | `hve-core-all`, `developer` | +| Infrastructure (IaC) | `hve-core-all`, `architect`, `devops` | +| Documentation standards | `hve-core-all`, `technical-writer` | +| Workflow instructions | `hve-core-all` plus relevant workflow personas | +| Test standards | `hve-core-all`, `developer` | +| ADO integration | `hve-core-all`, `tpm`, `devops` | + +### Tags for Instructions + +Common tags for instructions: + +| Tag | Use For | +|-------------------|------------------------------------------------| +| `language` | Programming language standards | +| `infrastructure` | IaC tools (Terraform, Bicep) | +| `documentation` | Writing and formatting standards | +| `testing` | Test code conventions | +| `ado` | Azure DevOps integration | +| `git` | Git workflow patterns | + +For complete registry documentation, see [AI Artifacts Common Standards - Artifact Registry](ai-artifacts-common.md#artifact-registry). + ## Content Structure Standards ### Required Sections diff --git a/docs/contributing/prompts.md b/docs/contributing/prompts.md index 7a6e5d0c..6d6e15ee 100644 --- a/docs/contributing/prompts.md +++ b/docs/contributing/prompts.md @@ -112,6 +112,53 @@ lastUpdated: '2025-11-19' --- ``` +## Registry Entry Requirements + +All prompts must have a corresponding entry in `.github/ai-artifacts-registry.json`. This entry controls distribution and persona filtering. + +### Adding Your Prompt to the Registry + +After creating your prompt file, add an entry to the `prompts` section of the registry: + +```json +"my-prompt": { + "maturity": "stable", + "personas": ["hve-core-all", "developer"], + "tags": ["workflow", "automation"] +} +``` + +### Selecting Personas for Prompts + +Choose personas based on who invokes or benefits from the workflow: + +| Prompt Type | Recommended Personas | +|----------------------------|----------------------------------------------| +| Git/PR workflows | `hve-core-all`, `developer` | +| ADO work item workflows | `hve-core-all`, `tpm`, `devops` | +| GitHub issue workflows | `hve-core-all`, `developer` | +| RPI workflow prompts | `hve-core-all` plus all relevant personas | +| Documentation workflows | `hve-core-all`, `technical-writer` | +| Architecture prompts | `hve-core-all`, `architect` | + +### Tags for Prompts + +Common tags for prompts: + +| Tag | Use For | +|---------------------|--------------------------------------| +| `rpi` | Research-Plan-Implement workflow | +| `git` | Git operations | +| `github` | GitHub-specific workflows | +| `ado` | Azure DevOps workflows | +| `planning` | Planning and estimation | +| `implementation` | Code implementation | +| `review` | Review processes | +| `documentation` | Documentation generation | +| `prompt-engineering`| Prompt building and analysis | + +For complete registry documentation, see [AI Artifacts Common Standards - Artifact Registry](ai-artifacts-common.md#artifact-registry). + ## Prompt Content Structure Standards ### Required Sections diff --git a/docs/contributing/skills.md b/docs/contributing/skills.md index 34cef5c5..c9fb9477 100644 --- a/docs/contributing/skills.md +++ b/docs/contributing/skills.md @@ -90,6 +90,47 @@ description: 'Video-to-GIF conversion skill with FFmpeg two-pass optimization - --- ``` +## Registry Entry Requirements + +All skills must have a corresponding entry in `.github/ai-artifacts-registry.json`. This entry controls distribution and persona filtering. + +### Adding Your Skill to the Registry + +After creating your skill package, add an entry to the `skills` section of the registry: + +```json +"my-skill": { + "maturity": "stable", + "personas": ["hve-core-all", "developer"], + "tags": ["tooling", "media"] +} +``` + +### Selecting Personas for Skills + +Choose personas based on who uses the skill's utilities: + +| Skill Type | Recommended Personas | +|-----------------------|----------------------------------------------| +| Media processing | `hve-core-all` | +| Documentation tools | `hve-core-all`, `technical-writer` | +| Data processing | `hve-core-all`, `developer` | +| Infrastructure tools | `hve-core-all`, `devops`, `architect` | +| Code generation | `hve-core-all`, `developer` | + +### Tags for Skills + +Common tags for skills: + +| Tag | Use For | +|-------------|--------------------------------------| +| `media` | Image, video, audio processing | +| `tooling` | General development utilities | +| `data` | Data transformation | +| `testing` | Test automation utilities | + +For complete registry documentation, see [AI Artifacts Common Standards - Artifact Registry](ai-artifacts-common.md#artifact-registry). + ## SKILL.md Content Structure ### Required Sections diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 40304a10..731a0eb7 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -85,6 +85,32 @@ Answer these questions to find your recommended installation method: โญ **VS Code Extension** is the recommended method for most users who don't need customization. +## Collection Packages + +HVE-Core supports persona-based artifact collections tailored to specific roles: + +| Collection | Identifier | Description | +|--------------------|--------------------|-----------------------------------------| +| **Full** | `hve-core` | All artifacts (recommended for most) | +| **Developer** | `hve-developer` | Software engineering focus | + +### Extension Installation (Full Collection) + +The VS Code Marketplace extension installs the **full collection** containing all stable artifacts. This is the recommended approach for most users. + +### Clone Methods (Persona Filtering) + +Clone-based installation methods support persona filtering through the installer agent: + +1. Clone the repository using your preferred method +2. Run the `hve-core-installer` agent +3. In Phase 7 (Agent Customization), select your role-based collection + +The installer filters artifacts based on your selected persona, copying only relevant agents, prompts, and instructions to your project. + +> [!NOTE] +> Persona filtering requires the artifact registry (`.github/ai-artifacts-registry.json`). The installer agent uses this registry to determine which artifacts belong to each collection. + ### Quick Decision Tree ```text diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index 8f9e03f9..29b1151f 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -254,6 +254,132 @@ When packaging, agents are filtered by their `maturity` frontmatter field: See [Agent Maturity Levels](../docs/contributing/ai-artifacts-common.md#maturity-field-requirements) for contributor guidance on setting maturity levels. +## Collection-Based Packaging + +The extension supports building persona-specific collection packages from a single codebase. + +### Available Collections + +Collection manifests are defined in `extension/collections/`: + +| Collection | Manifest | Description | +|-------------|---------------------------------|------------------------------------------| +| Full | `hve-core-all.collection.json` | All artifacts regardless of persona | +| Developer | `developer.collection.json` | Software engineering focused artifacts | + +### Building Collection Packages + +To build a specific collection package: + +```bash +# Build the full collection (default) +pwsh ./scripts/extension/Prepare-Extension.ps1 +pwsh ./scripts/extension/Package-Extension.ps1 + +# Build a persona-specific collection +pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection developer +pwsh ./scripts/extension/Package-Extension.ps1 -Version "1.0.0" +``` + +The `-Collection` parameter filters artifacts based on the collection manifest's persona list. + +### Collection Resolution + +When building a collection, the system: + +1. Reads the collection manifest to get the target personas +2. Reads the artifact registry (`.github/ai-artifacts-registry.json`) +3. Includes artifacts where `personas` array contains any of the collection's personas +4. Includes all `hve-core-all` artifacts as the base set +5. Resolves artifact dependencies to ensure completeness + +### Testing Collection Builds Locally + +To verify artifact inclusion before publishing: + +```bash +# 1. Prepare with collection filtering +pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection developer -Verbose + +# 2. Check package.json for included artifacts +cat extension/package.json | jq '.contributes.chatAgents' + +# 3. Validate the registry +npm run lint:registry + +# 4. Build the package (dry run) +pwsh ./scripts/extension/Package-Extension.ps1 -Version "1.0.0-test" -WhatIf +``` + +### Troubleshooting Collection Builds + +**Missing artifacts in collection:** + +1. Verify the artifact has a registry entry in `.github/ai-artifacts-registry.json` +2. Check the `personas` array includes the collection's persona or `hve-core-all` +3. Run `npm run lint:registry` to validate registry consistency + +**Dependency not included:** + +1. Check the parent artifact's `requires` field in the registry +2. Ensure dependent artifacts exist and have valid registry entries +3. Dependencies are included regardless of persona filter + +**Validation errors:** + +```bash +# Run full registry validation +npm run lint:registry + +# Check for orphaned artifacts (in registry but no file) +npm run lint:registry -- --verbose +``` + +### Collection Manifest Schema + +Collection manifests follow this structure: + +```json +{ + "$schema": "../../scripts/linting/schemas/collection.schema.json", + "id": "developer", + "name": "hve-developer", + "displayName": "HVE Core - Developer Edition", + "description": "AI-powered coding agents curated for software engineers", + "personas": ["developer"] +} +``` + +| Field | Required | Description | +|---------------|----------|--------------------------------------------| +| `id` | Yes | Unique identifier for the collection | +| `name` | Yes | Extension package name | +| `displayName` | Yes | Marketplace display name | +| `description` | Yes | Marketplace description text | +| `personas` | Yes | Array of persona identifiers to include | + +### Adding New Collections + +To create a new persona collection: + +1. Create a new manifest in `extension/collections/`: + +```json +{ + "$schema": "../../scripts/linting/schemas/collection.schema.json", + "id": "my-persona", + "name": "hve-my-persona", + "displayName": "HVE Core - My Persona Edition", + "description": "Description of artifacts included for this persona", + "personas": ["my-persona"] +} +``` + +1. Add the persona to the registry's `personas` section +2. Tag relevant artifacts with the new persona in the registry +3. Test the build locally with `-Collection my-persona` +4. Submit PR with the new collection manifest + ## Notes * The `.github`, `docs/templates`, and `scripts/dev-tools` folders are temporarily copied during packaging (not permanently stored) From f7e9e24e4812ad447ff45c7895693d8d4280cff2 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 17:31:53 +0100 Subject: [PATCH 11/62] fix(docs): linting tables across documentation --- docs/architecture/ai-artifacts.md | 20 ++++++------ docs/contributing/custom-agents.md | 16 ++++----- docs/contributing/instructions.md | 32 +++++++++--------- docs/contributing/prompts.md | 38 +++++++++++----------- docs/contributing/skills.md | 26 +++++++-------- docs/getting-started/install.md | 8 ++--- extension/PACKAGING.md | 52 +++++++++++++++--------------- 7 files changed, 96 insertions(+), 96 deletions(-) diff --git a/docs/architecture/ai-artifacts.md b/docs/architecture/ai-artifacts.md index ab8d6422..af4554f1 100644 --- a/docs/architecture/ai-artifacts.md +++ b/docs/architecture/ai-artifacts.md @@ -250,21 +250,21 @@ Each artifact entry contains metadata for filtering and dependency resolution: } ``` -| Field | Purpose | -|------------|------------------------------------------------------| -| `maturity` | Controls extension channel inclusion | -| `personas` | Determines collection membership | -| `tags` | Categorization for organization and discovery | -| `requires` | Declares dependencies for complete installation | +| Field | Purpose | +|------------|-------------------------------------------------| +| `maturity` | Controls extension channel inclusion | +| `personas` | Determines collection membership | +| `tags` | Categorization for organization and discovery | +| `requires` | Declares dependencies for complete installation | ### Persona Model Personas represent user roles that consume artifacts. The registry defines these personas: -| Persona | Identifier | Target Users | -|---------------|----------------|----------------------| -| **All** | `hve-core-all` | Universal inclusion | -| **Developer** | `developer` | Software engineers | +| Persona | Identifier | Target Users | +|---------------|----------------|---------------------| +| **All** | `hve-core-all` | Universal inclusion | +| **Developer** | `developer` | Software engineers | Artifacts assigned to `hve-core-all` appear in the full collection and may also include role-specific personas for targeted distribution. diff --git a/docs/contributing/custom-agents.md b/docs/contributing/custom-agents.md index 3d85aa75..81d8044b 100644 --- a/docs/contributing/custom-agents.md +++ b/docs/contributing/custom-agents.md @@ -226,14 +226,14 @@ After creating your agent file, add an entry to the `agents` section of the regi Choose personas based on who benefits most from your agent: -| Agent Type | Recommended Personas | -|---------------------------|---------------------------------------------| -| Task workflow agents | `hve-core-all`, `developer`, `tpm` | -| Architecture agents | `hve-core-all`, `architect`, `devops` | -| Documentation agents | `hve-core-all`, `technical-writer` | -| Data science agents | `hve-core-all`, `developer` | -| ADO/work item agents | `hve-core-all`, `tpm`, `devops` | -| Code review agents | `hve-core-all`, `developer` | +| Agent Type | Recommended Personas | +|----------------------|---------------------------------------| +| Task workflow agents | `hve-core-all`, `developer`, `tpm` | +| Architecture agents | `hve-core-all`, `architect`, `devops` | +| Documentation agents | `hve-core-all`, `technical-writer` | +| Data science agents | `hve-core-all`, `developer` | +| ADO/work item agents | `hve-core-all`, `tpm`, `devops` | +| Code review agents | `hve-core-all`, `developer` | ### Declaring Agent Dependencies diff --git a/docs/contributing/instructions.md b/docs/contributing/instructions.md index d693aa0c..d2b64af9 100644 --- a/docs/contributing/instructions.md +++ b/docs/contributing/instructions.md @@ -144,27 +144,27 @@ For instructions in subdirectories, use the path format: Choose personas based on who uses the technology or pattern: -| Instruction Type | Recommended Personas | -|---------------------------|---------------------------------------------------| -| Language standards | `hve-core-all`, `developer` | -| Infrastructure (IaC) | `hve-core-all`, `architect`, `devops` | -| Documentation standards | `hve-core-all`, `technical-writer` | -| Workflow instructions | `hve-core-all` plus relevant workflow personas | -| Test standards | `hve-core-all`, `developer` | -| ADO integration | `hve-core-all`, `tpm`, `devops` | +| Instruction Type | Recommended Personas | +|-------------------------|------------------------------------------------| +| Language standards | `hve-core-all`, `developer` | +| Infrastructure (IaC) | `hve-core-all`, `architect`, `devops` | +| Documentation standards | `hve-core-all`, `technical-writer` | +| Workflow instructions | `hve-core-all` plus relevant workflow personas | +| Test standards | `hve-core-all`, `developer` | +| ADO integration | `hve-core-all`, `tpm`, `devops` | ### Tags for Instructions Common tags for instructions: -| Tag | Use For | -|-------------------|------------------------------------------------| -| `language` | Programming language standards | -| `infrastructure` | IaC tools (Terraform, Bicep) | -| `documentation` | Writing and formatting standards | -| `testing` | Test code conventions | -| `ado` | Azure DevOps integration | -| `git` | Git workflow patterns | +| Tag | Use For | +|------------------|----------------------------------| +| `language` | Programming language standards | +| `infrastructure` | IaC tools (Terraform, Bicep) | +| `documentation` | Writing and formatting standards | +| `testing` | Test code conventions | +| `ado` | Azure DevOps integration | +| `git` | Git workflow patterns | For complete registry documentation, see [AI Artifacts Common Standards - Artifact Registry](ai-artifacts-common.md#artifact-registry). diff --git a/docs/contributing/prompts.md b/docs/contributing/prompts.md index 6d6e15ee..94d4463c 100644 --- a/docs/contributing/prompts.md +++ b/docs/contributing/prompts.md @@ -132,30 +132,30 @@ After creating your prompt file, add an entry to the `prompts` section of the re Choose personas based on who invokes or benefits from the workflow: -| Prompt Type | Recommended Personas | -|----------------------------|----------------------------------------------| -| Git/PR workflows | `hve-core-all`, `developer` | -| ADO work item workflows | `hve-core-all`, `tpm`, `devops` | -| GitHub issue workflows | `hve-core-all`, `developer` | -| RPI workflow prompts | `hve-core-all` plus all relevant personas | -| Documentation workflows | `hve-core-all`, `technical-writer` | -| Architecture prompts | `hve-core-all`, `architect` | +| Prompt Type | Recommended Personas | +|-------------------------|-------------------------------------------| +| Git/PR workflows | `hve-core-all`, `developer` | +| ADO work item workflows | `hve-core-all`, `tpm`, `devops` | +| GitHub issue workflows | `hve-core-all`, `developer` | +| RPI workflow prompts | `hve-core-all` plus all relevant personas | +| Documentation workflows | `hve-core-all`, `technical-writer` | +| Architecture prompts | `hve-core-all`, `architect` | ### Tags for Prompts Common tags for prompts: -| Tag | Use For | -|---------------------|--------------------------------------| -| `rpi` | Research-Plan-Implement workflow | -| `git` | Git operations | -| `github` | GitHub-specific workflows | -| `ado` | Azure DevOps workflows | -| `planning` | Planning and estimation | -| `implementation` | Code implementation | -| `review` | Review processes | -| `documentation` | Documentation generation | -| `prompt-engineering`| Prompt building and analysis | +| Tag | Use For | +|----------------------|----------------------------------| +| `rpi` | Research-Plan-Implement workflow | +| `git` | Git operations | +| `github` | GitHub-specific workflows | +| `ado` | Azure DevOps workflows | +| `planning` | Planning and estimation | +| `implementation` | Code implementation | +| `review` | Review processes | +| `documentation` | Documentation generation | +| `prompt-engineering` | Prompt building and analysis | For complete registry documentation, see [AI Artifacts Common Standards - Artifact Registry](ai-artifacts-common.md#artifact-registry). diff --git a/docs/contributing/skills.md b/docs/contributing/skills.md index c9fb9477..ca63e7e2 100644 --- a/docs/contributing/skills.md +++ b/docs/contributing/skills.md @@ -110,24 +110,24 @@ After creating your skill package, add an entry to the `skills` section of the r Choose personas based on who uses the skill's utilities: -| Skill Type | Recommended Personas | -|-----------------------|----------------------------------------------| -| Media processing | `hve-core-all` | -| Documentation tools | `hve-core-all`, `technical-writer` | -| Data processing | `hve-core-all`, `developer` | -| Infrastructure tools | `hve-core-all`, `devops`, `architect` | -| Code generation | `hve-core-all`, `developer` | +| Skill Type | Recommended Personas | +|----------------------|---------------------------------------| +| Media processing | `hve-core-all` | +| Documentation tools | `hve-core-all`, `technical-writer` | +| Data processing | `hve-core-all`, `developer` | +| Infrastructure tools | `hve-core-all`, `devops`, `architect` | +| Code generation | `hve-core-all`, `developer` | ### Tags for Skills Common tags for skills: -| Tag | Use For | -|-------------|--------------------------------------| -| `media` | Image, video, audio processing | -| `tooling` | General development utilities | -| `data` | Data transformation | -| `testing` | Test automation utilities | +| Tag | Use For | +|-----------|--------------------------------| +| `media` | Image, video, audio processing | +| `tooling` | General development utilities | +| `data` | Data transformation | +| `testing` | Test automation utilities | For complete registry documentation, see [AI Artifacts Common Standards - Artifact Registry](ai-artifacts-common.md#artifact-registry). diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 731a0eb7..6b047de1 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -89,10 +89,10 @@ Answer these questions to find your recommended installation method: HVE-Core supports persona-based artifact collections tailored to specific roles: -| Collection | Identifier | Description | -|--------------------|--------------------|-----------------------------------------| -| **Full** | `hve-core` | All artifacts (recommended for most) | -| **Developer** | `hve-developer` | Software engineering focus | +| Collection | Identifier | Description | +|---------------|-----------------|--------------------------------------| +| **Full** | `hve-core` | All artifacts (recommended for most) | +| **Developer** | `hve-developer` | Software engineering focus | ### Extension Installation (Full Collection) diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index 29b1151f..cbb479e3 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -262,10 +262,10 @@ The extension supports building persona-specific collection packages from a sing Collection manifests are defined in `extension/collections/`: -| Collection | Manifest | Description | -|-------------|---------------------------------|------------------------------------------| -| Full | `hve-core-all.collection.json` | All artifacts regardless of persona | -| Developer | `developer.collection.json` | Software engineering focused artifacts | +| Collection | Manifest | Description | +|------------|--------------------------------|----------------------------------------| +| Full | `hve-core-all.collection.json` | All artifacts regardless of persona | +| Developer | `developer.collection.json` | Software engineering focused artifacts | ### Building Collection Packages @@ -350,13 +350,13 @@ Collection manifests follow this structure: } ``` -| Field | Required | Description | -|---------------|----------|--------------------------------------------| -| `id` | Yes | Unique identifier for the collection | -| `name` | Yes | Extension package name | -| `displayName` | Yes | Marketplace display name | -| `description` | Yes | Marketplace description text | -| `personas` | Yes | Array of persona identifiers to include | +| Field | Required | Description | +|---------------|----------|-----------------------------------------| +| `id` | Yes | Unique identifier for the collection | +| `name` | Yes | Extension package name | +| `displayName` | Yes | Marketplace display name | +| `description` | Yes | Marketplace description text | +| `personas` | Yes | Array of persona identifiers to include | ### Adding New Collections @@ -364,21 +364,21 @@ To create a new persona collection: 1. Create a new manifest in `extension/collections/`: -```json -{ - "$schema": "../../scripts/linting/schemas/collection.schema.json", - "id": "my-persona", - "name": "hve-my-persona", - "displayName": "HVE Core - My Persona Edition", - "description": "Description of artifacts included for this persona", - "personas": ["my-persona"] -} -``` - -1. Add the persona to the registry's `personas` section -2. Tag relevant artifacts with the new persona in the registry -3. Test the build locally with `-Collection my-persona` -4. Submit PR with the new collection manifest + ```json + { + "$schema": "../../scripts/linting/schemas/collection.schema.json", + "id": "my-persona", + "name": "hve-my-persona", + "displayName": "HVE Core - My Persona Edition", + "description": "Description of artifacts included for this persona", + "personas": ["my-persona"] + } + ``` + +2. Add the persona to the registry's `personas` section +3. Tag relevant artifacts with the new persona in the registry +4. Test the build locally with `-Collection my-persona` +5. Submit PR with the new collection manifest ## Notes From 38cc1b6600c5068f12edfa26f0aeeda530252688 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 19:52:28 +0100 Subject: [PATCH 12/62] feat(extension): add developer edition with persona-specific README and package template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - introduce package.developer.json for developer edition - create README.developer.md for developer edition features - update packaging scripts to handle persona README swapping - enhance version synchronization for persona templates ๐Ÿ”ง - Generated by Copilot --- .gitignore | 1 + extension/.vscodeignore | 6 ++ extension/PACKAGING.md | 56 ++++++++++-- extension/README.developer.md | 102 +++++++++++++++++++++ extension/package.developer.json | 24 +++++ release-please-config.json | 5 ++ scripts/extension/Package-Extension.ps1 | 114 ++++++++++++++++++++++++ scripts/extension/Prepare-Extension.ps1 | 29 ++++-- 8 files changed, 323 insertions(+), 14 deletions(-) create mode 100644 extension/README.developer.md create mode 100644 extension/package.developer.json diff --git a/.gitignore b/.gitignore index 4fd260af..188a933d 100644 --- a/.gitignore +++ b/.gitignore @@ -418,6 +418,7 @@ FodyWeavers.xsd # Extension build artifacts extension/LICENSE extension/CHANGELOG.md +extension/package.json.bak # Windows Installer files from build outputs *.cab diff --git a/extension/.vscodeignore b/extension/.vscodeignore index 926b9083..ac649063 100644 --- a/extension/.vscodeignore +++ b/extension/.vscodeignore @@ -13,3 +13,9 @@ !README.md !LICENSE !CHANGELOG.md + +# Exclude persona-specific READMEs (only the canonical README.md ships) +README.*.md + +# Exclude persona-specific package templates (only the canonical package.json ships) +package.*.json diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index cbb479e3..74dfae89 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -2,7 +2,7 @@ title: Extension Packaging Guide description: Developer guide for packaging and publishing the HVE Core VS Code extension author: Microsoft -ms.date: 2025-12-19 +ms.date: 2026-02-06 ms.topic: reference --- @@ -267,21 +267,65 @@ Collection manifests are defined in `extension/collections/`: | Full | `hve-core-all.collection.json` | All artifacts regardless of persona | | Developer | `developer.collection.json` | Software engineering focused artifacts | +### Persona Template Files + +Each persona collection has a corresponding `package.{collection-id}.json` template file in `extension/`. These files contain static metadata (name, display name, description, publisher) for the persona edition. The `contributes` section is empty because `Prepare-Extension.ps1` populates it dynamically at build time. + +| Template | Collection | Purpose | +| ------------------------ | ---------- | --------------------------------- | +| `package.json` | Full | Canonical manifest (hve-core-all) | +| `package.developer.json` | Developer | Developer edition metadata | + +The canonical `extension/package.json` serves double duty: it is both the default build target and the `hve-core-all` template. No separate `package.hve-core-all.json` file exists. + +When building a persona collection, `Prepare-Extension.ps1`: + +1. Backs up `package.json` to `package.json.bak` +2. Copies the persona template (`package.developer.json`) over `package.json` +3. Generates `contributes` into the copied file +4. Serializes the result as `package.json` + +After packaging, `Package-Extension.ps1` restores the canonical `package.json` from backup in its `finally` block. + +#### Version Synchronization + +Template files contain a `version` field managed by `release-please`. The `release-please-config.json` file includes `extra-files` entries for each template, ensuring versions stay synchronized across all persona templates and the canonical `package.json`. + ### Building Collection Packages To build a specific collection package: ```bash -# Build the full collection (default) +# Build the full collection (default, no template copy) pwsh ./scripts/extension/Prepare-Extension.ps1 pwsh ./scripts/extension/Package-Extension.ps1 -# Build a persona-specific collection -pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection developer -pwsh ./scripts/extension/Package-Extension.ps1 -Version "1.0.0" +# Build a persona-specific collection (copies persona template) +pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection extension/collections/developer.collection.json +pwsh ./scripts/extension/Package-Extension.ps1 -Collection extension/collections/developer.collection.json +``` + +When `-Collection` targets a persona other than `hve-core-all`, the prepare script copies the persona template to `package.json` before generating `contributes`. The packaging script restores the canonical `package.json` after building. + +### Inner Dev Loop + +For rapid iteration without running the full build pipeline, copy the persona template manually: + +```bash +# 1. Copy the developer template +cp extension/package.developer.json extension/package.json + +# 2. Run prepare to generate contributes +pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection extension/collections/developer.collection.json + +# 3. Inspect the result +cat extension/package.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['name'], len(d.get('contributes',{}).get('chatAgents',[])),'agents')" + +# 4. Restore canonical package.json +git checkout extension/package.json ``` -The `-Collection` parameter filters artifacts based on the collection manifest's persona list. +The template file stays clean. Use `git checkout extension/package.json` to restore the canonical state at any time. ### Collection Resolution diff --git a/extension/README.developer.md b/extension/README.developer.md new file mode 100644 index 00000000..ee072a3a --- /dev/null +++ b/extension/README.developer.md @@ -0,0 +1,102 @@ +# HVE Core - Developer Edition + +> AI-powered coding agents and prompts curated for software engineers + +HVE Core - Developer Edition provides a focused collection of AI chat agents, prompts, and instructions designed for software engineers working in VS Code with GitHub Copilot. This edition includes the RPI (Research-Plan-Implement) workflow and supporting development tools. + +## Features + +### ๐Ÿค– Chat Agents + +| Agent | Description | +| ----- | ----------- | +| **memory** | Conversation memory persistence for session continuity | +| **rpi-agent** | Autonomous RPI orchestrator dispatching task agents through Research, Plan, Implement, Review, and Discover phases | +| **task-implementor** | Executes implementation plans with progressive tracking and change records | +| **task-planner** | Implementation planner for creating actionable implementation plans | +| **task-researcher** | Task research specialist for comprehensive project analysis | +| **task-reviewer** | Reviews completed implementation work for accuracy, completeness, and convention compliance | + +### ๐Ÿ“ Prompts + +| Prompt | Description | +| ------ | ----------- | +| **checkpoint** | Save or restore conversation context using memory files | +| **rpi** | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks | +| **task-implement** | Locates and executes implementation plans using task-implementor mode | +| **task-plan** | Initiates implementation planning based on user context or research documents | +| **task-research** | Initiates research for implementation planning based on user requirements | +| **task-review** | Initiates implementation review based on user context or automatic artifact discovery | + +### ๐Ÿ“š Instructions + +| Instruction | Description | +| ----------- | ----------- | +| **commit-message** | Required instructions for creating all commit messages | +| **markdown** | Required instructions for creating or editing any Markdown files | + +### โšก Skills + +| Skill | Description | +| ----- | ----------- | +| **video-to-gif** | Video-to-GIF conversion with FFmpeg two-pass optimization | + +## Getting Started + +After installing this extension, the chat agents will be available in GitHub Copilot Chat. You can: + +1. **Use custom agents** by selecting the custom agent from the agent picker drop-down list in Copilot Chat +2. **Apply prompts** through the Copilot Chat interface +3. **Reference instructions** โ€” They're automatically applied based on file patterns + +### Post-Installation Setup + +Some chat agents create workflow artifacts in your project directory. See the [installation guide](https://github.com/microsoft/hve-core/blob/main/docs/getting-started/install.md#post-installation-update-your-gitignore) for recommended `.gitignore` configuration and other setup details. + +## Usage Examples + +### Using Chat Agents + +```plaintext +rpi-agent help me research and implement this feature end-to-end +task-planner help me break down this feature into implementable tasks +task-researcher investigate the best approach for adding authentication +``` + +### Applying Prompts + +Prompts are available in the Copilot Chat prompt picker and can be used to generate consistent, high-quality outputs for common tasks. + +## Pre-release Channel + +HVE Core offers two installation channels: + +| Channel | Description | Maturity Levels | +| ----------- | ------------------------------------------------------- | ----------------------------------- | +| Stable | Production-ready artifacts only | `stable` | +| Pre-release | Early access to new features and experimental artifacts | `stable`, `preview`, `experimental` | + +To install the pre-release version, select **Install Pre-Release Version** from the extension page in VS Code, or use the Extensions view and switch to the pre-release channel. + +For more details on maturity levels and the release process, see the [contributing documentation](https://github.com/microsoft/hve-core/blob/main/docs/contributing/release-process.md#extension-channels-and-maturity). + +## Requirements + +- VS Code version 1.106.1 or higher +- GitHub Copilot extension + +## Full Edition + +Looking for more agents covering architecture, documentation, Azure DevOps, data science, and security? Check out the full [HVE Core](https://marketplace.visualstudio.com/items?itemName=ise-hve-essentials.hve-core) extension. + +## License + +MIT License - see [LICENSE](LICENSE) for details + +## Support + +For issues, questions, or contributions, please visit the [GitHub repository](https://github.com/microsoft/hve-core). + +--- + +Brought to you by Microsoft ISE HVE Essentials diff --git a/extension/package.developer.json b/extension/package.developer.json new file mode 100644 index 00000000..10748ef3 --- /dev/null +++ b/extension/package.developer.json @@ -0,0 +1,24 @@ +{ + "name": "hve-developer", + "displayName": "HVE Core - Developer Edition", + "extensionKind": [ + "workspace", + "ui" + ], + "version": "2.1.0", + "description": "AI-powered coding agents and prompts curated for software engineers", + "publisher": "ise-hve-essentials", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/hve-core.git" + }, + "engines": { + "vscode": "^1.106.1" + }, + "categories": [ + "Chat" + ], + "contributes": {}, + "author": "Microsoft", + "license": "MIT" +} diff --git a/release-please-config.json b/release-please-config.json index 191f2c80..6c11a09a 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -23,6 +23,11 @@ "type": "json", "path": "extension/package.json", "jsonpath": "$.version" + }, + { + "type": "json", + "path": "extension/package.developer.json", + "jsonpath": "$.version" } ] } diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index 6bbe0a24..9be40b6a 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -285,6 +285,47 @@ function New-PackagingResult { } } +function Get-PersonaReadmePath { + <# + .SYNOPSIS + Resolves the persona-specific README path from a collection manifest. + .DESCRIPTION + Maps a collection manifest to its persona-specific README file. Returns + null when the collection is the full package (hve-core-all) or when no + matching persona README exists on disk. + .PARAMETER CollectionPath + Path to the collection manifest JSON file. + .PARAMETER ExtensionDirectory + Path to the extension directory containing README files. + .OUTPUTS + String path to the persona README, or $null if not applicable. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [string]$CollectionPath, + + [Parameter(Mandatory = $true)] + [string]$ExtensionDirectory + ) + + $manifest = Get-Content -Path $CollectionPath -Raw | ConvertFrom-Json + $collectionId = $manifest.id + + # Full package uses the default README.md + if ($collectionId -eq 'hve-core-all') { + return $null + } + + $personaReadmePath = Join-Path $ExtensionDirectory "README.$collectionId.md" + if (Test-Path $personaReadmePath) { + return $personaReadmePath + } + + return $null +} + function Get-ResolvedPackageVersion { <# .SYNOPSIS @@ -541,6 +582,55 @@ function Copy-CollectionArtifacts { } } +function Set-PersonaReadme { + <# + .SYNOPSIS + Swaps or restores the persona-specific README for collection packaging. + .DESCRIPTION + In swap mode, backs up the original README.md and copies the persona + README in its place. In restore mode, copies the backup back and removes it. + .PARAMETER ExtensionDirectory + Path to the extension directory. + .PARAMETER PersonaReadmePath + Path to the persona-specific README file. Required for Swap operation. + .PARAMETER Operation + Either 'Swap' to replace README.md with persona content, or 'Restore' + to revert README.md from backup. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ExtensionDirectory, + + [Parameter(Mandatory = $false)] + [string]$PersonaReadmePath = "", + + [Parameter(Mandatory = $true)] + [ValidateSet('Swap', 'Restore')] + [string]$Operation + ) + + $readmePath = Join-Path $ExtensionDirectory "README.md" + $backupPath = Join-Path $ExtensionDirectory "README.md.bak" + + if ($Operation -eq 'Swap') { + if (-not $PersonaReadmePath -or $PersonaReadmePath -eq "") { + Write-Warning "No persona README path provided for swap operation" + return + } + Copy-Item -Path $readmePath -Destination $backupPath -Force + Copy-Item -Path $PersonaReadmePath -Destination $readmePath -Force + Write-Host " Swapped README.md with $(Split-Path $PersonaReadmePath -Leaf)" -ForegroundColor Green + } + elseif ($Operation -eq 'Restore') { + if (Test-Path $backupPath) { + Copy-Item -Path $backupPath -Destination $readmePath -Force + Remove-Item -Path $backupPath -Force + Write-Host " Restored original README.md" -ForegroundColor Green + } + } +} + function Invoke-VsceCommand { <# .SYNOPSIS @@ -867,6 +957,16 @@ function Invoke-PackageExtension { Write-Host " โœ… Extension directory prepared" -ForegroundColor Green + # Swap persona README if collection specifies one + if ($Collection -and $Collection -ne "") { + $personaReadmePath = Get-PersonaReadmePath -CollectionPath $Collection -ExtensionDirectory $ExtensionDirectory + if ($personaReadmePath) { + Write-Host "" + Write-Host "๐Ÿ“„ Applying persona README..." -ForegroundColor Yellow + Set-PersonaReadme -ExtensionDirectory $ExtensionDirectory -PersonaReadmePath $personaReadmePath -Operation Swap + } + } + # Check vsce availability using pure function $vsceAvailability = Test-VsceAvailable if (-not $vsceAvailability.IsAvailable) { @@ -926,6 +1026,20 @@ function Invoke-PackageExtension { return New-PackagingResult -Success $false -ErrorMessage $_.Exception.Message } finally { + # Restore canonical package.json from persona template backup + $backupPath = Join-Path $ExtensionDirectory "package.json.bak" + if (Test-Path $backupPath) { + Copy-Item -Path $backupPath -Destination $PackageJsonPath -Force + Remove-Item -Path $backupPath -Force + Write-Host " Restored canonical package.json from backup" -ForegroundColor Green + + # Re-read restored package.json for downstream restore steps + $packageJson = Get-Content -Path $PackageJsonPath -Raw | ConvertFrom-Json + } + + # Restore persona README if it was swapped + Set-PersonaReadme -ExtensionDirectory $ExtensionDirectory -Operation Restore + # Cleanup copied directories using I/O function Write-Host "" Write-Host "๐Ÿงน Cleaning up..." -ForegroundColor Yellow diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index 3bb6f0c8..68024f80 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -1203,20 +1203,33 @@ function Invoke-PrepareExtension { Write-Host "Skills after filter: $($chatSkills.Count)" } - # Update package.json + # Apply persona template when building a non-default collection + if ($null -ne $collectionManifest -and $collectionManifest.id -ne 'hve-core-all') { + $collectionId = $collectionManifest.id + $templatePath = Join-Path $ExtensionDirectory "package.$collectionId.json" + if (-not (Test-Path $templatePath)) { + return New-PrepareResult -Success $false -ErrorMessage "Persona template not found: $templatePath" + } + + # Back up canonical package.json for later restore + $backupPath = Join-Path $ExtensionDirectory "package.json.bak" + Copy-Item -Path $PackageJsonPath -Destination $backupPath -Force + + # Copy persona template over package.json + Copy-Item -Path $templatePath -Destination $PackageJsonPath -Force + + # Re-read template as the working package.json + $packageJson = Get-Content -Path $PackageJsonPath -Raw | ConvertFrom-Json + Write-Host "Applied persona template: package.$collectionId.json" -ForegroundColor Green + } + + # Update package.json with generated contributes $packageJson = Update-PackageJsonContributes -PackageJson $packageJson ` -ChatAgents $chatAgents ` -ChatPromptFiles $chatPrompts ` -ChatInstructions $chatInstructions ` -ChatSkills $chatSkills - # Override package.json metadata from collection manifest - if ($null -ne $collectionManifest) { - if ($collectionManifest.ContainsKey('name')) { $packageJson.name = $collectionManifest.name } - if ($collectionManifest.ContainsKey('displayName')) { $packageJson.displayName = $collectionManifest.displayName } - if ($collectionManifest.ContainsKey('description')) { $packageJson.description = $collectionManifest.description } - } - # Write updated package.json if (-not $DryRun) { $packageJson | ConvertTo-Json -Depth 10 | Set-Content -Path $PackageJsonPath -Encoding UTF8NoBOM From 2c15920e1ff73d79c67f8c87492ceadd3499d4b5 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 20:16:11 +0100 Subject: [PATCH 13/62] test(extension): add Pester coverage for Option B template-copy and backup restore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add 5 template-copy branch tests to Prepare-Extension.Tests.ps1 - add 4 backup/restore branch tests to Package-Extension.Tests.ps1 ๐Ÿงช - Generated by Copilot --- .../extension/Package-Extension.Tests.ps1 | 122 +++++++++++++++ .../extension/Prepare-Extension.Tests.ps1 | 143 ++++++++++++++++++ 2 files changed, 265 insertions(+) diff --git a/scripts/tests/extension/Package-Extension.Tests.ps1 b/scripts/tests/extension/Package-Extension.Tests.ps1 index 42a92af6..eb8831f3 100644 --- a/scripts/tests/extension/Package-Extension.Tests.ps1 +++ b/scripts/tests/extension/Package-Extension.Tests.ps1 @@ -492,6 +492,128 @@ Describe 'Invoke-PackageExtension' { $result.Success | Should -BeFalse $result.ErrorMessage | Should -Match 'CIHelpers.psm1 not found' } + + Context 'Package.json backup restore' { + It 'Does not create backup when no collection specified' { + Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } } + Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } } + + $manifest = @{ + name = 'test-ext' + version = '1.0.0' + publisher = 'test' + engines = @{ vscode = '^1.80.0' } + } + $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json') + + # Create fake vsix so packaging succeeds + $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix' + Set-Content -Path $vsixPath -Value 'fake-vsix' + + $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot + + Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse + } + + It 'Restores package.json from backup after packaging' { + Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } } + Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } } + + # Original package.json content (will be overwritten by template) + $originalManifest = @{ + name = 'hve-core' + version = '1.0.0' + publisher = 'test' + engines = @{ vscode = '^1.80.0' } + } + + # Simulate post-template state: template content in package.json, original backed up + $templateManifest = @{ + name = 'hve-developer' + version = '1.0.0' + publisher = 'test' + engines = @{ vscode = '^1.80.0' } + } + $templateManifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json') + $originalManifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json.bak') + + # Create fake vsix so packaging succeeds + $vsixPath = Join-Path $script:extDir 'hve-developer-1.0.0.vsix' + Set-Content -Path $vsixPath -Value 'fake-vsix' + + $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot + + # Verify the original manifest was restored + $restored = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json + $restored.name | Should -Be 'hve-core' + } + + It 'Removes backup file after restore' { + Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } } + Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } } + + $manifest = @{ + name = 'test-ext' + version = '1.0.0' + publisher = 'test' + engines = @{ vscode = '^1.80.0' } + } + $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json') + + # Create a backup file manually to simulate Invoke-PrepareExtension behavior + $backupManifest = @{ + name = 'original-ext' + version = '1.0.0' + publisher = 'test' + engines = @{ vscode = '^1.80.0' } + } + $backupManifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json.bak') + + # Create fake vsix so packaging succeeds + $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix' + Set-Content -Path $vsixPath -Value 'fake-vsix' + + $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot + + Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse + } + + It 'Restored package.json contains original metadata' { + Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } } + Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } } + + # Original manifest backed up before template was applied + $originalManifest = @{ + name = 'hve-core-original' + version = '2.5.0' + publisher = 'original-pub' + description = 'Original description' + engines = @{ vscode = '^1.80.0' } + } + + # Template manifest currently in package.json + $templateManifest = @{ + name = 'hve-persona' + version = '2.5.0' + publisher = 'persona-pub' + description = 'Persona description' + engines = @{ vscode = '^1.80.0' } + } + $templateManifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json') + $originalManifest | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $script:extDir 'package.json.bak') + + # Create fake vsix matching the template name + $vsixPath = Join-Path $script:extDir 'hve-persona-2.5.0.vsix' + Set-Content -Path $vsixPath -Value 'fake-vsix' + + $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot + + $restored = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json + $restored.name | Should -Be 'hve-core-original' + $restored.publisher | Should -Be 'original-pub' + $restored.description | Should -Be 'Original description' + } + } } Describe 'Test-PackagingInputsValid' { diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index 34e2b8d5..f02b2dea 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -949,4 +949,147 @@ applyTo: "**/*.js" $result.Success | Should -BeFalse $result.ErrorMessage | Should -Match 'Invalid version format' } + + Context 'Persona template copy' { + BeforeAll { + # Developer collection manifest + $script:devCollectionPath = Join-Path $script:tempDir 'developer.collection.json' + @{ + id = 'developer' + name = 'hve-developer' + displayName = 'HVE Core - Developer Edition' + description = 'Developer edition' + personas = @('developer') + } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:devCollectionPath + + # hve-core-all collection manifest (default) + $script:allCollectionPath = Join-Path $script:tempDir 'hve-core-all.collection.json' + @{ + id = 'hve-core-all' + name = 'hve-core-all' + displayName = 'HVE Core - All' + description = 'All artifacts' + personas = @('hve-core-all') + } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:allCollectionPath + + # Collection manifest referencing a missing template + $script:missingCollectionPath = Join-Path $script:tempDir 'nonexistent.collection.json' + @{ + id = 'nonexistent' + name = 'nonexistent' + displayName = 'Nonexistent' + description = 'Missing template' + personas = @('nonexistent') + } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:missingCollectionPath + + # Persona template for developer collection + @' +{ + "name": "hve-developer", + "version": "1.2.3", + "contributes": {} +} +'@ | Set-Content -Path (Join-Path $script:extDir 'package.developer.json') + + # Update registry with developer and nonexistent persona entries + $registryContent = @{ + version = '1.0' + personas = @{ + definitions = @{ + 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } + 'developer' = @{ name = 'Developer'; description = 'Developer artifacts' } + 'nonexistent' = @{ name = 'Nonexistent'; description = 'Missing template' } + } + } + agents = @{ + 'test' = @{ maturity = 'stable'; personas = @('hve-core-all', 'developer', 'nonexistent'); tags = @() } + } + prompts = @{ + 'test' = @{ maturity = 'stable'; personas = @('hve-core-all', 'developer', 'nonexistent'); tags = @() } + } + instructions = @{ + 'test' = @{ maturity = 'stable'; personas = @('hve-core-all', 'developer', 'nonexistent'); tags = @() } + } + skills = @{} + } + $registryContent | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:ghDir 'ai-artifacts-registry.json') + } + + BeforeEach { + $script:originalPackageJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw + } + + AfterEach { + $script:originalPackageJson | Set-Content -Path (Join-Path $script:extDir 'package.json') + $bakPath = Join-Path $script:extDir 'package.json.bak' + if (Test-Path $bakPath) { + Remove-Item -Path $bakPath -Force + } + } + + It 'Skips template copy when no collection specified' { + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' ` + -DryRun + + $result.Success | Should -BeTrue + $currentContent = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw + $currentContent | Should -Be $script:originalPackageJson + Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse + } + + It 'Skips template copy for hve-core-all collection' { + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' ` + -Collection $script:allCollectionPath ` + -DryRun + + $result.Success | Should -BeTrue + Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse + } + + It 'Returns error when persona template file missing' { + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' ` + -Collection $script:missingCollectionPath ` + -DryRun + + $result.Success | Should -BeFalse + $result.ErrorMessage | Should -Match 'Persona template not found' + } + + It 'Copies template to package.json for non-default collection' { + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' ` + -Collection $script:devCollectionPath ` + -DryRun + + $result.Success | Should -BeTrue + $updatedJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json + $updatedJson.name | Should -Be 'hve-developer' + } + + It 'Creates package.json.bak backup before template copy' { + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' ` + -Collection $script:devCollectionPath ` + -DryRun + + $result.Success | Should -BeTrue + $bakPath = Join-Path $script:extDir 'package.json.bak' + Test-Path $bakPath | Should -BeTrue + $bakContent = Get-Content -Path $bakPath -Raw + $bakContent | Should -Be $script:originalPackageJson + } + } } From 3eae81420027de739a3fb1de1f09305596fba8f4 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 20:19:19 +0100 Subject: [PATCH 14/62] feat(extension): add skills directory to packaging specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ”ง - Generated by Copilot --- scripts/extension/Package-Extension.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index 9be40b6a..d40f2665 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -494,6 +494,11 @@ function Get-PackagingDirectorySpec { Source = Join-Path $RepoRoot "docs/templates" Destination = Join-Path $ExtensionDirectory "docs/templates" IsFile = $false + }, + @{ + Source = Join-Path $RepoRoot ".github/skills" + Destination = Join-Path $ExtensionDirectory ".github/skills" + IsFile = $false } ) } From c0a832b44ea271d1dedc5eb3b4a9f226a666f9c7 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 20:32:30 +0100 Subject: [PATCH 15/62] feat(workflows): enhance extension packaging and publishing workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add discover-collections job to package workflow - implement template consistency validation in Prepare-Extension script - update publish job to handle collection-specific VSIX files - improve error handling for missing VSIX files ๐Ÿ”ง - Generated by Copilot --- .github/workflows/extension-package.yml | 71 ++++++++++++++----- .github/workflows/extension-publish.yml | 55 ++++++++++++--- .github/workflows/main.yml | 45 ++++++++++-- scripts/extension/Prepare-Extension.ps1 | 93 +++++++++++++++++++++++++ 4 files changed, 233 insertions(+), 31 deletions(-) diff --git a/.github/workflows/extension-package.yml b/.github/workflows/extension-package.yml index c9b1997d..5e1ae5ca 100644 --- a/.github/workflows/extension-package.yml +++ b/.github/workflows/extension-package.yml @@ -27,21 +27,56 @@ on: version: description: 'Version that was packaged' value: ${{ jobs.package.outputs.version }} - vsix-file: - description: 'Path to the packaged VSIX file' - value: ${{ jobs.package.outputs.vsix-file }} permissions: contents: read jobs: + discover-collections: + name: Discover Collection Manifests + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + matrix: ${{ steps.discover.outputs.matrix }} + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 + with: + persist-credentials: false + + - name: Discover collection manifests + id: discover + shell: bash + run: | + collections_dir="extension/collections" + if [ ! -d "$collections_dir" ]; then + echo "::error::Collections directory not found: $collections_dir" + exit 1 + fi + + # Build JSON array of collection manifests + matrix_json=$(find "$collections_dir" -name '*.collection.json' -type f | sort | while IFS= read -r file; do + id=$(jq -r '.id' "$file") + name=$(jq -r '.name' "$file") + echo "{\"id\":\"$id\",\"name\":\"$name\",\"manifest\":\"$file\"}" + done | jq -s '{include: .}') + + echo "matrix=$matrix_json" >> "$GITHUB_OUTPUT" + echo "Discovered collections:" + echo "$matrix_json" | jq . + package: + name: Package ${{ matrix.id }} + needs: discover-collections runs-on: ubuntu-latest permissions: contents: read + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.discover-collections.outputs.matrix) }} outputs: version: ${{ steps.package.outputs.version }} - vsix-file: ${{ steps.package.outputs.vsix-file }} steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 @@ -76,9 +111,10 @@ jobs: run: | $channel = "${{ inputs.channel }}".Trim() if (-not $channel) { $channel = 'Stable' } - - Write-Host "๐Ÿ”ง Preparing extension for $channel channel..." - ./scripts/extension/Prepare-Extension.ps1 -Channel $channel + $collection = "${{ matrix.manifest }}" + + Write-Host "๐Ÿ”ง Preparing extension for $channel channel (collection: ${{ matrix.id }})..." + ./scripts/extension/Prepare-Extension.ps1 -Channel $channel -Collection $collection - name: Package extension id: package @@ -86,28 +122,31 @@ jobs: run: | $version = "${{ inputs.version }}".Trim() $devPatch = "${{ inputs.dev-patch-number }}".Trim() - - Write-Host "๐Ÿ“ฆ Packaging extension..." - - $arguments = @{} - + $collection = "${{ matrix.manifest }}" + + Write-Host "๐Ÿ“ฆ Packaging extension (collection: ${{ matrix.id }})..." + + $arguments = @{ + Collection = $collection + } + if ($version) { $arguments['Version'] = $version } - + if ($devPatch) { $arguments['DevPatchNumber'] = $devPatch } - + if (Test-Path "./CHANGELOG.md") { $arguments['ChangelogPath'] = "./CHANGELOG.md" } - + ./scripts/extension/Package-Extension.ps1 @arguments - name: Upload VSIX artifact uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4.4.3 with: - name: extension-vsix + name: extension-vsix-${{ matrix.id }} path: extension/*.vsix retention-days: 30 diff --git a/.github/workflows/extension-publish.yml b/.github/workflows/extension-publish.yml index fb4b429c..e5beab28 100644 --- a/.github/workflows/extension-publish.yml +++ b/.github/workflows/extension-publish.yml @@ -74,7 +74,7 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" package: - name: Package Extension + name: Package Extensions needs: [prepare-changelog, normalize-version] uses: ./.github/workflows/extension-package.yml with: @@ -83,12 +83,43 @@ jobs: permissions: contents: read + discover-vsix: + name: Discover VSIX Artifacts + needs: [package] + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.discover.outputs.matrix }} + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 + with: + persist-credentials: false + + - name: Discover collection artifacts + id: discover + shell: bash + run: | + # Build publish matrix from collection manifests + collections_dir="extension/collections" + matrix_json=$(find "$collections_dir" -name '*.collection.json' -type f | sort | while IFS= read -r file; do + id=$(jq -r '.id' "$file") + name=$(jq -r '.name' "$file") + echo "{\"id\":\"$id\",\"name\":\"$name\"}" + done | jq -s '{include: .}') + + echo "matrix=$matrix_json" >> "$GITHUB_OUTPUT" + echo "Discovered collections for publish:" + echo "$matrix_json" | jq . + publish: - name: Publish to Marketplace - needs: [prepare-changelog, package] + name: Publish ${{ matrix.id }} + needs: [package, discover-vsix] if: ${{ !inputs.dry-run }} runs-on: ubuntu-latest environment: marketplace + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.discover-vsix.outputs.matrix) }} permissions: contents: read id-token: write @@ -116,22 +147,26 @@ jobs: - name: Download VSIX artifact uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: - name: extension-vsix - path: ./extension + name: extension-vsix-${{ matrix.id }} + path: ./dist - name: Publish to VS Code Marketplace run: | - VSIX_FILE=$(find extension -name 'hve-core-*.vsix' -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2) - echo "๐Ÿ“ฆ Publishing: $VSIX_FILE" + VSIX_FILE=$(find dist -name '*.vsix' -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2) + if [ -z "$VSIX_FILE" ]; then + echo "::error::No VSIX file found for collection ${{ matrix.id }}" + exit 1 + fi + echo "๐Ÿ“ฆ Publishing ${{ matrix.id }}: $VSIX_FILE" vsce publish --packagePath "$VSIX_FILE" --azure-credential - name: Summary run: | { - echo "## ๐ŸŽ‰ Extension Published Successfully" + echo "## ๐ŸŽ‰ Extension Published: ${{ matrix.name }}" echo "" + echo "**Collection:** ${{ matrix.id }}" echo "**Version:** ${{ needs.package.outputs.version }}" - echo "**VSIX File:** ${{ needs.package.outputs.vsix-file }}" echo "" - echo "View on [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=ise-hve-essentials.hve-core)" + echo "View on [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=ise-hve-essentials.${{ matrix.name }})" } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9f68278e..fb6129f0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -97,7 +97,7 @@ jobs: manifest-file: .release-please-manifest.json extension-package-release: - name: Package VS Code Extension (Release) + name: Package VS Code Extensions (Release) needs: [release-please] if: ${{ needs.release-please.outputs.release_created == 'true' }} uses: ./.github/workflows/extension-package.yml @@ -106,11 +106,38 @@ jobs: permissions: contents: read - attest-and-upload: - name: Attest and Upload Release Assets + discover-release-collections: + name: Discover Release Collections needs: [release-please, extension-package-release] if: ${{ needs.release-please.outputs.release_created == 'true' }} runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.discover.outputs.matrix }} + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 + with: + persist-credentials: false + + - name: Discover collection manifests + id: discover + shell: bash + run: | + collections_dir="extension/collections" + matrix_json=$(find "$collections_dir" -name '*.collection.json' -type f | sort | while IFS= read -r file; do + id=$(jq -r '.id' "$file") + echo "{\"id\":\"$id\"}" + done | jq -s '{include: .}') + echo "matrix=$matrix_json" >> "$GITHUB_OUTPUT" + + attest-and-upload: + name: Attest and Upload (${{ matrix.id }}) + needs: [release-please, extension-package-release, discover-release-collections] + if: ${{ needs.release-please.outputs.release_created == 'true' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.discover-release-collections.outputs.matrix) }} permissions: contents: write id-token: write @@ -119,7 +146,7 @@ jobs: - name: Download VSIX artifact uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: - name: extension-vsix + name: extension-vsix-${{ matrix.id }} path: ./dist - name: Attest build provenance @@ -133,11 +160,19 @@ jobs: run: | VSIX_FILE=$(find dist -name '*.vsix' | head -1) if [ -z "$VSIX_FILE" ]; then - echo "::error::No VSIX file found in dist/" + echo "::error::No VSIX file found for collection ${{ matrix.id }}" exit 1 fi gh release upload "${{ needs.release-please.outputs.tag_name }}" "$VSIX_FILE" --clobber -R "${{ github.repository }}" + publish-release: + name: Publish GitHub Release + needs: [release-please, attest-and-upload] + if: ${{ needs.release-please.outputs.release_created == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: - name: Publish GitHub Release env: GH_TOKEN: ${{ github.token }} diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index 68024f80..2634f977 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -1006,6 +1006,89 @@ function New-PrepareResult { } } +function Test-TemplateConsistency { + <# + .SYNOPSIS + Validates persona template metadata against its collection manifest. + .DESCRIPTION + Compares name, displayName, and description fields between a persona + package template (e.g. package.developer.json) and the corresponding + collection manifest. Emits warnings for divergences and returns a list + of mismatches. + .PARAMETER TemplatePath + Path to the persona package template JSON file. + .PARAMETER CollectionManifest + Parsed collection manifest hashtable with name, displayName, description. + .OUTPUTS + [hashtable] With Mismatches array and IsConsistent bool. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$TemplatePath, + + [Parameter(Mandatory = $true)] + [hashtable]$CollectionManifest + ) + + $result = @{ + Mismatches = @() + IsConsistent = $true + } + + if (-not (Test-Path $TemplatePath)) { + $result.Mismatches += @{ + Field = 'file' + Template = $TemplatePath + Manifest = 'N/A' + Message = "Template file not found: $TemplatePath" + } + $result.IsConsistent = $false + return $result + } + + try { + $template = Get-Content -Path $TemplatePath -Raw | ConvertFrom-Json + } + catch { + $result.Mismatches += @{ + Field = 'file' + Template = $TemplatePath + Manifest = 'N/A' + Message = "Failed to parse template: $($_.Exception.Message)" + } + $result.IsConsistent = $false + return $result + } + + $fieldsToCheck = @('name', 'displayName', 'description') + foreach ($field in $fieldsToCheck) { + $templateValue = $null + $manifestValue = $null + + if ($template.PSObject.Properties[$field]) { + $templateValue = $template.$field + } + if ($CollectionManifest.ContainsKey($field)) { + $manifestValue = $CollectionManifest[$field] + } + + if ($null -ne $templateValue -and $null -ne $manifestValue -and $templateValue -ne $manifestValue) { + $result.Mismatches += @{ + Field = $field + Template = $templateValue + Manifest = $manifestValue + Message = "$field diverges: template='$templateValue' manifest='$manifestValue'" + } + $result.IsConsistent = $false + } + } + + return $result +} + function Invoke-PrepareExtension { <# .SYNOPSIS @@ -1211,6 +1294,16 @@ function Invoke-PrepareExtension { return New-PrepareResult -Success $false -ErrorMessage "Persona template not found: $templatePath" } + # Validate template consistency against collection manifest + $consistency = Test-TemplateConsistency -TemplatePath $templatePath -CollectionManifest $collectionManifest + if (-not $consistency.IsConsistent) { + Write-Host "`n--- Template Consistency Warnings ---" -ForegroundColor Yellow + foreach ($mismatch in $consistency.Mismatches) { + Write-Warning "Template/manifest mismatch: $($mismatch.Message)" + Write-CIAnnotation -Message "Template/manifest mismatch ($collectionId): $($mismatch.Message)" -Level Warning + } + } + # Back up canonical package.json for later restore $backupPath = Join-Path $ExtensionDirectory "package.json.bak" Copy-Item -Path $PackageJsonPath -Destination $backupPath -Force From 14c8225f77b8396574579ff64f52d40eb947c542 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 6 Feb 2026 20:35:15 +0100 Subject: [PATCH 16/62] feat(workflows): extract version from VSIX filename for clarity --- .github/workflows/extension-publish.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/extension-publish.yml b/.github/workflows/extension-publish.yml index e5beab28..4a3f5713 100644 --- a/.github/workflows/extension-publish.yml +++ b/.github/workflows/extension-publish.yml @@ -151,13 +151,17 @@ jobs: path: ./dist - name: Publish to VS Code Marketplace + id: publish run: | VSIX_FILE=$(find dist -name '*.vsix' -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2) if [ -z "$VSIX_FILE" ]; then echo "::error::No VSIX file found for collection ${{ matrix.id }}" exit 1 fi - echo "๐Ÿ“ฆ Publishing ${{ matrix.id }}: $VSIX_FILE" + # Extract version from VSIX filename to avoid depending on matrix job output + VSIX_VERSION=$(basename "$VSIX_FILE" .vsix | grep -oE '[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$') + echo "version=$VSIX_VERSION" >> "$GITHUB_OUTPUT" + echo "๐Ÿ“ฆ Publishing ${{ matrix.id }}: $VSIX_FILE (v$VSIX_VERSION)" vsce publish --packagePath "$VSIX_FILE" --azure-credential - name: Summary @@ -166,7 +170,7 @@ jobs: echo "## ๐ŸŽ‰ Extension Published: ${{ matrix.name }}" echo "" echo "**Collection:** ${{ matrix.id }}" - echo "**Version:** ${{ needs.package.outputs.version }}" + echo "**Version:** ${{ steps.publish.outputs.version }}" echo "" echo "View on [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=ise-hve-essentials.${{ matrix.name }})" } >> "$GITHUB_STEP_SUMMARY" From fca292077a5aff72ef0e154f32136fa65646f22f Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 08:33:07 +0100 Subject: [PATCH 17/62] feat(agents): add GitHub backlog management instructions and prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - introduce new backlog management agents and their corresponding instructions - update maturity levels for existing prompts and instructions - clarify repo-specific instruction exclusions in documentation ๐Ÿ”ง - Generated by Copilot --- .../agents/github-backlog-manager.agent.md | 1 - .github/ai-artifacts-registry.json | 133 +++++++++++++++++- .../community-interaction.instructions.md | 1 - .../github-backlog-discovery.instructions.md | 1 - .../github-backlog-planning.instructions.md | 1 - .../github-backlog-triage.instructions.md | 1 - .../github-backlog-update.instructions.md | 1 - .../prompts/github-discover-issues.prompt.md | 1 - .../prompts/github-execute-backlog.prompt.md | 1 - .github/prompts/github-sprint-plan.prompt.md | 1 - .../prompts/github-triage-issues.prompt.md | 1 - .github/prompts/incident-response.prompt.md | 1 - docs/architecture/ai-artifacts.md | 11 +- docs/contributing/ai-artifacts-common.md | 11 ++ docs/contributing/instructions.md | 14 +- extension/PACKAGING.md | 1 + scripts/extension/Prepare-Extension.ps1 | 6 + scripts/linting/Validate-ArtifactRegistry.ps1 | 4 +- 18 files changed, 173 insertions(+), 18 deletions(-) diff --git a/.github/agents/github-backlog-manager.agent.md b/.github/agents/github-backlog-manager.agent.md index db189307..a6fece71 100644 --- a/.github/agents/github-backlog-manager.agent.md +++ b/.github/agents/github-backlog-manager.agent.md @@ -1,6 +1,5 @@ --- description: "Orchestrator agent for GitHub backlog management workflows including triage, discovery, sprint planning, and execution - Brought to you by microsoft/hve-core" -maturity: experimental tools: - github/* - search diff --git a/.github/ai-artifacts-registry.json b/.github/ai-artifacts-registry.json index 75dedd90..2b227e80 100644 --- a/.github/ai-artifacts-registry.json +++ b/.github/ai-artifacts-registry.json @@ -160,6 +160,37 @@ "skills": [] } }, + "github-backlog-manager": { + "maturity": "experimental", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github", + "backlog", + "orchestration" + ], + "requires": { + "agents": [ + "memory" + ], + "prompts": [ + "github-discover-issues", + "github-triage-issues", + "github-sprint-plan", + "github-execute-backlog", + "checkpoint" + ], + "instructions": [ + "github-backlog-discovery", + "github-backlog-planning", + "github-backlog-triage", + "github-backlog-update", + "community-interaction" + ], + "skills": [] + } + }, "github-issue-manager": { "maturity": "stable", "personas": [ @@ -504,7 +535,7 @@ ] }, "github-add-issue": { - "maturity": "stable", + "maturity": "deprecated", "personas": [ "hve-core-all" ], @@ -512,6 +543,56 @@ "github" ] }, + "github-discover-issues": { + "maturity": "experimental", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github", + "backlog" + ] + }, + "github-execute-backlog": { + "maturity": "experimental", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github", + "backlog" + ] + }, + "github-sprint-plan": { + "maturity": "experimental", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github", + "backlog" + ] + }, + "github-triage-issues": { + "maturity": "experimental", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github", + "backlog" + ] + }, + "incident-response": { + "maturity": "stable", + "personas": [ + "hve-core-all" + ], + "tags": [ + "operations", + "incident" + ] + }, "prompt-analyze": { "maturity": "stable", "personas": [ @@ -670,6 +751,16 @@ "git" ] }, + "community-interaction": { + "maturity": "experimental", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github", + "documentation" + ] + }, "git-merge": { "maturity": "stable", "personas": [ @@ -679,6 +770,46 @@ "git" ] }, + "github-backlog-discovery": { + "maturity": "experimental", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github", + "backlog" + ] + }, + "github-backlog-planning": { + "maturity": "experimental", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github", + "backlog" + ] + }, + "github-backlog-triage": { + "maturity": "experimental", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github", + "backlog" + ] + }, + "github-backlog-update": { + "maturity": "experimental", + "personas": [ + "hve-core-all" + ], + "tags": [ + "github", + "backlog" + ] + }, "hve-core-location": { "maturity": "stable", "personas": [ diff --git a/.github/instructions/community-interaction.instructions.md b/.github/instructions/community-interaction.instructions.md index 3c1a5894..ec8dd947 100644 --- a/.github/instructions/community-interaction.instructions.md +++ b/.github/instructions/community-interaction.instructions.md @@ -1,7 +1,6 @@ --- description: 'Community interaction voice, tone, and response templates for GitHub-facing agents and prompts' applyTo: '**/.github/instructions/github-backlog-*.instructions.md' -maturity: experimental --- # Community Interaction Guidelines diff --git a/.github/instructions/github-backlog-discovery.instructions.md b/.github/instructions/github-backlog-discovery.instructions.md index 65f7b200..6d66b6ff 100644 --- a/.github/instructions/github-backlog-discovery.instructions.md +++ b/.github/instructions/github-backlog-discovery.instructions.md @@ -1,7 +1,6 @@ --- description: 'Discovery protocol for GitHub backlog management - artifact-driven, user-centric, and search-based issue discovery' applyTo: '**/.copilot-tracking/github-issues/discovery/**' -maturity: experimental --- # GitHub Backlog Discovery diff --git a/.github/instructions/github-backlog-planning.instructions.md b/.github/instructions/github-backlog-planning.instructions.md index dbbdd4be..15aaee4e 100644 --- a/.github/instructions/github-backlog-planning.instructions.md +++ b/.github/instructions/github-backlog-planning.instructions.md @@ -1,7 +1,6 @@ --- description: 'Reference specification for GitHub backlog management tooling - planning files, search protocols, similarity assessment, and state persistence' applyTo: '**/.copilot-tracking/github-issues/**' -maturity: experimental --- # GitHub Backlog Planning File Instructions diff --git a/.github/instructions/github-backlog-triage.instructions.md b/.github/instructions/github-backlog-triage.instructions.md index 388bd33e..c8100723 100644 --- a/.github/instructions/github-backlog-triage.instructions.md +++ b/.github/instructions/github-backlog-triage.instructions.md @@ -1,7 +1,6 @@ --- description: 'Triage workflow for GitHub issue backlog management - automated label suggestion, milestone assignment, and duplicate detection' applyTo: '**/.copilot-tracking/github-issues/triage/**' -maturity: experimental --- # GitHub Backlog Triage Instructions diff --git a/.github/instructions/github-backlog-update.instructions.md b/.github/instructions/github-backlog-update.instructions.md index 36ab5a7e..989e63ee 100644 --- a/.github/instructions/github-backlog-update.instructions.md +++ b/.github/instructions/github-backlog-update.instructions.md @@ -1,7 +1,6 @@ --- description: 'Execution workflow for GitHub issue backlog management - consumes planning handoffs and executes issue operations' applyTo: '**/.copilot-tracking/github-issues/**/handoff-logs.md' -maturity: experimental --- # GitHub Backlog Update Instructions diff --git a/.github/prompts/github-discover-issues.prompt.md b/.github/prompts/github-discover-issues.prompt.md index d893da52..efb21deb 100644 --- a/.github/prompts/github-discover-issues.prompt.md +++ b/.github/prompts/github-discover-issues.prompt.md @@ -2,7 +2,6 @@ description: 'Discover GitHub issues through user-centric queries, artifact-driven analysis, or search-based exploration and produce planning files for review' agent: 'github-backlog-manager' argument-hint: "documents=... [milestone=...] [searchTerms=...]" -maturity: experimental --- # Discover GitHub Issues diff --git a/.github/prompts/github-execute-backlog.prompt.md b/.github/prompts/github-execute-backlog.prompt.md index 940a7986..6092193a 100644 --- a/.github/prompts/github-execute-backlog.prompt.md +++ b/.github/prompts/github-execute-backlog.prompt.md @@ -2,7 +2,6 @@ description: 'Execute a GitHub backlog plan by creating, updating, linking, closing, and commenting on issues from a handoff file' agent: 'github-backlog-manager' argument-hint: "handoff=... [autonomy={full|partial|manual}] [dryRun={true|false}]" -maturity: experimental --- # Execute GitHub Backlog Plan diff --git a/.github/prompts/github-sprint-plan.prompt.md b/.github/prompts/github-sprint-plan.prompt.md index e76d9f86..a66c552d 100644 --- a/.github/prompts/github-sprint-plan.prompt.md +++ b/.github/prompts/github-sprint-plan.prompt.md @@ -2,7 +2,6 @@ description: 'Plan a GitHub milestone sprint by analyzing issue coverage, identifying gaps, and organizing work into a prioritized sprint backlog' agent: 'github-backlog-manager' argument-hint: "milestone=... [documents=...] [sprintGoal=...] [capacity=...] [autonomy={full|partial|manual}]" -maturity: experimental --- # Plan GitHub Sprint diff --git a/.github/prompts/github-triage-issues.prompt.md b/.github/prompts/github-triage-issues.prompt.md index dc65128a..8176ddce 100644 --- a/.github/prompts/github-triage-issues.prompt.md +++ b/.github/prompts/github-triage-issues.prompt.md @@ -1,7 +1,6 @@ --- description: 'Triage GitHub issues not yet triaged with automated label suggestions, milestone assignment, and duplicate detection' agent: 'github-backlog-manager' -maturity: experimental --- # Triage GitHub Issues diff --git a/.github/prompts/incident-response.prompt.md b/.github/prompts/incident-response.prompt.md index f646a31c..13e68b03 100644 --- a/.github/prompts/incident-response.prompt.md +++ b/.github/prompts/incident-response.prompt.md @@ -1,7 +1,6 @@ --- description: "Incident response workflow for Azure operations scenarios - Brought to you by microsoft/hve-core" name: incident-response -maturity: stable argument-hint: "[incident-description] [severity={1|2|3|4}] [phase={triage|diagnose|mitigate|rca}]" --- diff --git a/docs/architecture/ai-artifacts.md b/docs/architecture/ai-artifacts.md index af4554f1..48e1171b 100644 --- a/docs/architecture/ai-artifacts.md +++ b/docs/architecture/ai-artifacts.md @@ -83,6 +83,13 @@ maturity: 'stable' Instructions answer the question "what standards apply to this context?" and ensure consistent code quality. +#### Repo-Specific Instructions + +Instructions placed in `.github/instructions/hve-core/` are scoped to the hve-core repository itself and MUST NOT be registered as AI artifacts. These files govern internal repository concerns (CI/CD workflows, repo-specific conventions) that are not applicable outside the repository. The build system and registry validation automatically exclude this subdirectory from artifact discovery and orphan detection. + +> [!IMPORTANT] +> The `.github/instructions/hve-core/` directory is reserved for repo-specific instructions. Files in this directory are never distributed through extension packages or persona collections. + ### Skills Skills (`.github/skills//SKILL.md`) provide executable utilities that agents invoke for specialized tasks. Unlike instructions (passive reference), skills contain actual scripts that perform operations. @@ -316,10 +323,10 @@ The extension scans these directories at startup: * `.github/prompts/` for workflow entry points * `.github/agents/` for specialized behaviors -* `.github/instructions/` for technology standards +* `.github/instructions/` for technology standards (excluding `hve-core/` subdirectory) * `.github/skills/` for utility packages -Artifact inclusion is controlled by the registry: +Artifact inclusion is controlled by the registry. Repo-specific instructions under `.github/instructions/hve-core/` are excluded from discovery and never packaged into extension builds. | Maturity | Stable Channel | Pre-release Channel | |----------------|----------------|---------------------| diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 5ef0d902..9e932e92 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -148,6 +148,17 @@ When contributing a new artifact: 4. Declare any dependencies in `requires` if the artifact depends on other artifacts 5. Run `npm run lint:registry` to validate the registry entry +### Repo-Specific Instructions Exclusion + +Instructions placed in `.github/instructions/hve-core/` are repo-specific and MUST NOT be added to the registry. These files govern internal hve-core repository concerns (CI/CD workflows, repo-specific conventions) that do not apply outside this repository. They are excluded from: + +* The AI artifacts registry +* Extension packaging and distribution +* Persona collection builds +* Orphan detection in registry validation + +If your instructions apply only to the hve-core repository and are not intended for distribution to consumers, place them in `.github/instructions/hve-core/`. Otherwise, place them in `.github/instructions/` or a technology-specific subdirectory (e.g., `csharp/`, `bash/`). + ## Persona Taxonomy Personas represent user roles that consume HVE-Core artifacts. The persona system enables role-specific artifact collections without fragmenting the codebase. diff --git a/docs/contributing/instructions.md b/docs/contributing/instructions.md index d2b64af9..4507c9d9 100644 --- a/docs/contributing/instructions.md +++ b/docs/contributing/instructions.md @@ -36,10 +36,15 @@ All instruction files **MUST** be placed in: โ”œโ”€โ”€ language-name.instructions.md # Language-specific โ”œโ”€โ”€ framework-name.instructions.md # Framework-specific โ”œโ”€โ”€ workflow-name.instructions.md # Workflow-specific -โ””โ”€โ”€ subfolder/ - โ””โ”€โ”€ specialized.instructions.md # Organized by domain +โ”œโ”€โ”€ subfolder/ +โ”‚ โ””โ”€โ”€ specialized.instructions.md # Organized by domain +โ””โ”€โ”€ hve-core/ + โ””โ”€โ”€ repo-only.instructions.md # Repo-specific (NOT distributed) ``` +> [!IMPORTANT] +> The `.github/instructions/hve-core/` subdirectory is reserved for repo-specific instructions that apply only to the hve-core repository. Files in this directory are NOT registered as AI artifacts and are never distributed through extension packages or persona collections. Use this location for internal repository concerns such as CI/CD workflows or conventions that do not generalize to consumers. + **Examples**: * `.github/instructions/python-script.instructions.md` @@ -116,7 +121,10 @@ lastUpdated: '2025-11-19' ## Registry Entry Requirements -All instructions must have a corresponding entry in `.github/ai-artifacts-registry.json`. This entry controls distribution and persona filtering. +All instructions must have a corresponding entry in `.github/ai-artifacts-registry.json`, except for repo-specific instructions placed in `.github/instructions/hve-core/`. This entry controls distribution and persona filtering. + +> [!NOTE] +> Instructions in `.github/instructions/hve-core/` are repo-specific and MUST NOT be added to the registry. See [Repo-Specific Instructions Exclusion](ai-artifacts-common.md#repo-specific-instructions-exclusion) for details. ### Adding Your Instructions to the Registry diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index e4e98656..b4949519 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -444,6 +444,7 @@ To create a new persona collection: * The `.github`, `docs/templates`, and `scripts/dev-tools` folders are temporarily copied during packaging (not permanently stored) * `LICENSE` and `CHANGELOG.md` are copied from root during packaging and excluded from git * Only essential extension files are included (agents, prompts, instructions, templates, dev-tools) +* Repo-specific instructions under `.github/instructions/hve-core/` are excluded from all builds * Non-essential files are excluded (workflows, issue templates, agent installer, etc.) * The root `package.json` contains development scripts for the repository diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index 2634f977..0d0dbb9b 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -760,6 +760,12 @@ function Get-DiscoveredInstructions { $instructionFiles = Get-ChildItem -Path $InstructionsDir -Filter "*.instructions.md" -Recurse | Sort-Object Name foreach ($instrFile in $instructionFiles) { + # Skip repo-specific instructions not intended for distribution + $instrRelPath = [System.IO.Path]::GetRelativePath($InstructionsDir, $instrFile.FullName) -replace '\\', '/' + if ($instrRelPath -like 'hve-core/*') { + $result.Skipped += @{ Name = $instrFile.BaseName; Reason = 'repo-specific (hve-core/)' } + continue + } $baseName = $instrFile.BaseName -replace '\.instructions$', '' $instrName = "$baseName-instructions" $displayName = ($baseName -replace '-', ' ') -replace '(\b\w)', { $_.Groups[1].Value.ToUpper() } diff --git a/scripts/linting/Validate-ArtifactRegistry.ps1 b/scripts/linting/Validate-ArtifactRegistry.ps1 index e8b044a1..8f9b108c 100644 --- a/scripts/linting/Validate-ArtifactRegistry.ps1 +++ b/scripts/linting/Validate-ArtifactRegistry.ps1 @@ -402,12 +402,14 @@ function Find-OrphanArtifacts { } } - # Scan instructions (including subdirectories) + # Scan instructions (including subdirectories, excluding repo-specific hve-core/ folder) $instructionsDir = Join-Path $RepoRoot '.github/instructions' if (Test-Path $instructionsDir) { $instructionFiles = Get-ChildItem -Path $instructionsDir -Filter '*.instructions.md' -File -Recurse -ErrorAction SilentlyContinue foreach ($file in $instructionFiles) { $relativePath = [System.IO.Path]::GetRelativePath($instructionsDir, $file.FullName) -replace '\\', '/' + # Skip repo-specific instructions not intended for distribution + if ($relativePath -like 'hve-core/*') { continue } $key = $relativePath -replace '\.instructions\.md$', '' if (-not $Registry['instructions'].ContainsKey($key)) { $warnings.Add("Orphan instruction file not in registry: $($file.FullName)") From 2dc78a2d7e456c00c4891fb72d8235226536dbb1 Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 08:36:23 +0100 Subject: [PATCH 18/62] test(instructions): add tests for repo-specific instruction exclusion --- .../extension/Prepare-Extension.Tests.ps1 | 40 +++++++++++++++++++ .../Validate-ArtifactRegistry.Tests.ps1 | 31 ++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index f02b2dea..d365da6c 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -231,6 +231,46 @@ applyTo: "**/*.ps1" $result = Get-DiscoveredInstructions -InstructionsDir $nonexistentPath -GitHubDir $script:ghDir -AllowedMaturities @('stable') $result.DirectoryExists | Should -BeFalse } + + It 'Skips repo-specific instructions in hve-core subdirectory' { + $hveCoreDir = Join-Path $script:instrDir 'hve-core' + New-Item -ItemType Directory -Path $hveCoreDir -Force | Out-Null + @' +--- +description: "Repo-specific workflow instruction" +applyTo: "**/.github/workflows/*.yml" +--- +'@ | Set-Content -Path (Join-Path $hveCoreDir 'workflows.instructions.md') + + $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable') + $instrNames = $result.Instructions | ForEach-Object { $_.name } + $instrNames | Should -Not -Contain 'workflows-instructions' + $result.Skipped | Where-Object { $_.Reason -match 'repo-specific' } | Should -Not -BeNullOrEmpty + } + + It 'Still discovers instructions in other subdirectories' { + $hveCoreDir = Join-Path $script:instrDir 'hve-core' + $otherDir = Join-Path $script:instrDir 'csharp' + New-Item -ItemType Directory -Path $hveCoreDir -Force | Out-Null + New-Item -ItemType Directory -Path $otherDir -Force | Out-Null + @' +--- +description: "Repo-specific" +applyTo: "**/.github/workflows/*.yml" +--- +'@ | Set-Content -Path (Join-Path $hveCoreDir 'workflows.instructions.md') + @' +--- +description: "C# instruction" +applyTo: "**/*.cs" +--- +'@ | Set-Content -Path (Join-Path $otherDir 'csharp.instructions.md') + + $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('stable') + $instrNames = $result.Instructions | ForEach-Object { $_.name } + $instrNames | Should -Contain 'csharp-instructions' + $instrNames | Should -Not -Contain 'workflows-instructions' + } } Describe 'Get-RegistryData' { diff --git a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 index 2f27c680..3270a1aa 100644 --- a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 +++ b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 @@ -511,6 +511,37 @@ Describe 'Find-OrphanArtifacts' -Tag 'Unit' { } } + Context 'Repo-specific instruction exclusion' { + It 'Skips instruction files in hve-core subdirectory' { + New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/instructions/hve-core" -Force | Out-Null + Set-Content -Path "$script:OrphanTestRoot/.github/instructions/hve-core/workflows.instructions.md" -Value '# Repo-specific' + $registry = @{ + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot + $result.Warnings | Where-Object { $_ -match 'hve-core' } | Should -BeNullOrEmpty + } + + It 'Still detects orphan instructions in other subdirectories' { + New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/instructions/hve-core" -Force | Out-Null + New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/instructions/other" -Force | Out-Null + Set-Content -Path "$script:OrphanTestRoot/.github/instructions/hve-core/workflows.instructions.md" -Value '# Repo-specific' + Set-Content -Path "$script:OrphanTestRoot/.github/instructions/other/orphan.instructions.md" -Value '# Orphan' + $registry = @{ + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot + $result.Warnings | Where-Object { $_ -match 'other/orphan' } | Should -Not -BeNullOrEmpty + $result.Warnings | Where-Object { $_ -match 'hve-core' } | Should -BeNullOrEmpty + } + } + Context 'Missing directories' { It 'Handles missing artifact directories gracefully' { $emptyRepoRoot = Join-Path $TestDrive 'empty-repo' From 439ab4975a4c89a71fa97208c5127af44826330d Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 08:49:07 +0100 Subject: [PATCH 19/62] refactor(extension): generate contributes at build time with path-only entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - remove static contributes listing from extension/package.json - strip name and description from discovery outputs in Prepare-Extension.ps1 - remove Get-FrontmatterData function and its Pester tests - emit only path in generated chatAgents, chatPromptFiles, chatInstructions, chatSkills ๐Ÿ“ฆ - Generated by Copilot --- extension/package.json | 359 +----------------- scripts/extension/Prepare-Extension.ps1 | 82 +--- .../extension/Prepare-Extension.Tests.ps1 | 52 --- 3 files changed, 15 insertions(+), 478 deletions(-) diff --git a/extension/package.json b/extension/package.json index abba9a44..d184f3c5 100644 --- a/extension/package.json +++ b/extension/package.json @@ -18,364 +18,7 @@ "categories": [ "Chat" ], - "contributes": { - "chatAgents": [ - { - "name": "ado-prd-to-wit", - "path": "./.github/agents/ado-prd-to-wit.agent.md", - "description": "Product Manager expert for analyzing PRDs and planning Azure DevOps work item hierarchies" - }, - { - "name": "adr-creation", - "path": "./.github/agents/adr-creation.agent.md", - "description": "Interactive AI coaching for collaborative architectural decision record creation with guided discovery, research integration, and progressive documentation building - Brought to you by microsoft/edge-ai" - }, - { - "name": "arch-diagram-builder", - "path": "./.github/agents/arch-diagram-builder.agent.md", - "description": "Architecture diagram builder agent that builds high quality ASCII-art diagrams - Brought to you by microsoft/hve-core" - }, - { - "name": "brd-builder", - "path": "./.github/agents/brd-builder.agent.md", - "description": "Business Requirements Document builder with guided Q&A and reference integration" - }, - { - "name": "doc-ops", - "path": "./.github/agents/doc-ops.agent.md", - "description": "Autonomous documentation operations agent for pattern compliance, accuracy verification, and gap detection - Brought to you by microsoft/hve-core" - }, - { - "name": "gen-data-spec", - "path": "./.github/agents/gen-data-spec.agent.md", - "description": "Generate comprehensive data dictionaries, machine-readable data profiles, and objective summaries for downstream analysis (EDA notebooks, dashboards) through guided discovery" - }, - { - "name": "gen-jupyter-notebook", - "path": "./.github/agents/gen-jupyter-notebook.agent.md", - "description": "Create structured exploratory data analysis Jupyter notebooks from available data sources and generated data dictionaries" - }, - { - "name": "gen-streamlit-dashboard", - "path": "./.github/agents/gen-streamlit-dashboard.agent.md", - "description": "Develop a multi-page Streamlit dashboard" - }, - { - "name": "github-backlog-manager", - "path": "./.github/agents/github-backlog-manager.agent.md", - "description": "Orchestrator agent for GitHub backlog management workflows including triage, discovery, sprint planning, and execution - Brought to you by microsoft/hve-core" - }, - { - "name": "hve-core-installer", - "path": "./.github/agents/hve-core-installer.agent.md", - "description": "Decision-driven installer for HVE-Core with 6 installation methods for local, devcontainer, and Codespaces environments - Brought to you by microsoft/hve-core" - }, - { - "name": "memory", - "path": "./.github/agents/memory.agent.md", - "description": "Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core" - }, - { - "name": "pr-review", - "path": "./.github/agents/pr-review.agent.md", - "description": "Comprehensive Pull Request review assistant ensuring code quality, security, and convention compliance - Brought to you by microsoft/hve-core" - }, - { - "name": "prd-builder", - "path": "./.github/agents/prd-builder.agent.md", - "description": "Product Requirements Document builder with guided Q&A and reference integration" - }, - { - "name": "prompt-builder", - "path": "./.github/agents/prompt-builder.agent.md", - "description": "Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core" - }, - { - "name": "rpi-agent", - "path": "./.github/agents/rpi-agent.agent.md", - "description": "Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core" - }, - { - "name": "security-plan-creator", - "path": "./.github/agents/security-plan-creator.agent.md", - "description": "Expert security architect for creating comprehensive cloud security plans - Brought to you by microsoft/hve-core" - }, - { - "name": "task-implementor", - "path": "./.github/agents/task-implementor.agent.md", - "description": "Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records" - }, - { - "name": "task-planner", - "path": "./.github/agents/task-planner.agent.md", - "description": "Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core" - }, - { - "name": "task-researcher", - "path": "./.github/agents/task-researcher.agent.md", - "description": "Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core" - }, - { - "name": "task-reviewer", - "path": "./.github/agents/task-reviewer.agent.md", - "description": "Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core" - }, - { - "name": "test-streamlit-dashboard", - "path": "./.github/agents/test-streamlit-dashboard.agent.md", - "description": "Automated testing for Streamlit dashboards using Playwright with issue tracking and reporting" - } - ], - "chatPromptFiles": [ - { - "name": "ado-create-pull-request", - "path": "./.github/prompts/ado-create-pull-request.prompt.md", - "description": "Generate pull request description, discover related work items, identify reviewers, and create Azure DevOps pull request with all linkages." - }, - { - "name": "ado-get-build-info", - "path": "./.github/prompts/ado-get-build-info.prompt.md", - "description": "Retrieve Azure DevOps build information for a Pull Request or specific Build Number." - }, - { - "name": "ado-get-my-work-items", - "path": "./.github/prompts/ado-get-my-work-items.prompt.md", - "description": "Retrieve user's current Azure DevOps work items and organize them into planning file definitions" - }, - { - "name": "ado-process-my-work-items-for-task-planning", - "path": "./.github/prompts/ado-process-my-work-items-for-task-planning.prompt.md", - "description": "Process retrieved work items for task planning and generate task-planning-logs.md handoff file" - }, - { - "name": "ado-update-wit-items", - "path": "./.github/prompts/ado-update-wit-items.prompt.md", - "description": "Prompt to update work items based on planning files" - }, - { - "name": "checkpoint", - "path": "./.github/prompts/checkpoint.prompt.md", - "description": "Save or restore conversation context using memory files - Brought to you by microsoft/hve-core" - }, - { - "name": "doc-ops-update", - "path": "./.github/prompts/doc-ops-update.prompt.md", - "description": "Invoke doc-ops agent for documentation quality assurance and updates" - }, - { - "name": "git-commit-message", - "path": "./.github/prompts/git-commit-message.prompt.md", - "description": "Generates a commit message following the commit-message.instructions.md rules based on all changes in the branch" - }, - { - "name": "git-commit", - "path": "./.github/prompts/git-commit.prompt.md", - "description": "Stages all changes, generates a conventional commit message, shows it to the user, and commits using only git add/commit" - }, - { - "name": "git-merge", - "path": "./.github/prompts/git-merge.prompt.md", - "description": "Coordinate Git merge, rebase, and rebase --onto workflows with consistent conflict handling." - }, - { - "name": "git-setup", - "path": "./.github/prompts/git-setup.prompt.md", - "description": "Interactive, verification-first Git configuration assistant (non-destructive)" - }, - { - "name": "github-add-issue", - "path": "./.github/prompts/github-add-issue.prompt.md", - "description": "Create a GitHub issue using discovered repository templates and conversational field collection" - }, - { - "name": "github-discover-issues", - "path": "./.github/prompts/github-discover-issues.prompt.md", - "description": "Discover GitHub issues through user-centric queries, artifact-driven analysis, or search-based exploration and produce planning files for review" - }, - { - "name": "github-execute-backlog", - "path": "./.github/prompts/github-execute-backlog.prompt.md", - "description": "Execute a GitHub backlog plan by creating, updating, linking, closing, and commenting on issues from a handoff file" - }, - { - "name": "github-sprint-plan", - "path": "./.github/prompts/github-sprint-plan.prompt.md", - "description": "Plan a GitHub milestone sprint by analyzing issue coverage, identifying gaps, and organizing work into a prioritized sprint backlog" - }, - { - "name": "github-triage-issues", - "path": "./.github/prompts/github-triage-issues.prompt.md", - "description": "Triage GitHub issues not yet triaged with automated label suggestions, milestone assignment, and duplicate detection" - }, - { - "name": "prompt-analyze", - "path": "./.github/prompts/prompt-analyze.prompt.md", - "description": "Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core" - }, - { - "name": "prompt-build", - "path": "./.github/prompts/prompt-build.prompt.md", - "description": "Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core" - }, - { - "name": "prompt-refactor", - "path": "./.github/prompts/prompt-refactor.prompt.md", - "description": "Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core" - }, - { - "name": "pull-request", - "path": "./.github/prompts/pull-request.prompt.md", - "description": "Provides prompt instructions for pull request (PR) generation - Brought to you by microsoft/edge-ai" - }, - { - "name": "risk-register", - "path": "./.github/prompts/risk-register.prompt.md", - "description": "Creates a concise and well-structured qualitative risk register using a Probability ร— Impact (Pร—I) risk matrix." - }, - { - "name": "rpi", - "path": "./.github/prompts/rpi.prompt.md", - "description": "Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core" - }, - { - "name": "task-implement", - "path": "./.github/prompts/task-implement.prompt.md", - "description": "Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core" - }, - { - "name": "task-plan", - "path": "./.github/prompts/task-plan.prompt.md", - "description": "Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core" - }, - { - "name": "task-research", - "path": "./.github/prompts/task-research.prompt.md", - "description": "Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core" - }, - { - "name": "task-review", - "path": "./.github/prompts/task-review.prompt.md", - "description": "Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core" - } - ], - "chatInstructions": [ - { - "name": "ado-create-pull-request-instructions", - "path": "./.github/instructions/ado-create-pull-request.instructions.md", - "description": "Required protocol for creating Azure DevOps pull requests with work item discovery, reviewer identification, and automated linking." - }, - { - "name": "ado-get-build-info-instructions", - "path": "./.github/instructions/ado-get-build-info.instructions.md", - "description": "Required instructions for anything related to Azure Devops or ado build information including status, logs, or details from provided pullrequest (PR), build Id, or branch name." - }, - { - "name": "ado-update-wit-items-instructions", - "path": "./.github/instructions/ado-update-wit-items.instructions.md", - "description": "Work item creation and update protocol using MCP ADO tools with handoff tracking" - }, - { - "name": "ado-wit-discovery-instructions", - "path": "./.github/instructions/ado-wit-discovery.instructions.md", - "description": "Protocol for discovering Azure DevOps work items via user assignment or artifact analysis with planning file output" - }, - { - "name": "ado-wit-planning-instructions", - "path": "./.github/instructions/ado-wit-planning.instructions.md", - "description": "Reference specification for Azure DevOps work item planning files, templates, field definitions, and search protocols" - }, - { - "name": "bash-instructions", - "path": "./.github/instructions/bash/bash.instructions.md", - "description": "Instructions for bash script implementation - Brought to you by microsoft/edge-ai" - }, - { - "name": "bicep-instructions", - "path": "./.github/instructions/bicep/bicep.instructions.md", - "description": "Instructions for Bicep infrastructure as code implementation - Brought to you by microsoft/hve-core" - }, - { - "name": "commit-message-instructions", - "path": "./.github/instructions/commit-message.instructions.md", - "description": "Required instructions for creating all commit messages - Brought to you by microsoft/hve-core" - }, - { - "name": "community-interaction-instructions", - "path": "./.github/instructions/community-interaction.instructions.md", - "description": "Community interaction voice, tone, and response templates for GitHub-facing agents and prompts" - }, - { - "name": "csharp-tests-instructions", - "path": "./.github/instructions/csharp/csharp-tests.instructions.md", - "description": "Required instructions for C# (CSharp) test code research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core" - }, - { - "name": "csharp-instructions", - "path": "./.github/instructions/csharp/csharp.instructions.md", - "description": "Required instructions for C# (CSharp) research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core" - }, - { - "name": "git-merge-instructions", - "path": "./.github/instructions/git-merge.instructions.md", - "description": "Required protocol for Git merge, rebase, and rebase --onto workflows with conflict handling and stop controls." - }, - { - "name": "github-backlog-discovery-instructions", - "path": "./.github/instructions/github-backlog-discovery.instructions.md", - "description": "Discovery protocol for GitHub backlog management - artifact-driven, user-centric, and search-based issue discovery" - }, - { - "name": "github-backlog-planning-instructions", - "path": "./.github/instructions/github-backlog-planning.instructions.md", - "description": "Reference specification for GitHub backlog management tooling - planning files, search protocols, similarity assessment, and state persistence" - }, - { - "name": "github-backlog-triage-instructions", - "path": "./.github/instructions/github-backlog-triage.instructions.md", - "description": "Triage workflow for GitHub issue backlog management - automated label suggestion, milestone assignment, and duplicate detection" - }, - { - "name": "github-backlog-update-instructions", - "path": "./.github/instructions/github-backlog-update.instructions.md", - "description": "Execution workflow for GitHub issue backlog management - consumes planning handoffs and executes issue operations" - }, - { - "name": "hve-core-location-instructions", - "path": "./.github/instructions/hve-core-location.instructions.md", - "description": "Important: hve-core is the repository containing this instruction file; Guidance: if a referenced prompt, instructions, agent, or script is missing in the current directory, fall back to this hve-core location by walking up this file's directory tree." - }, - { - "name": "markdown-instructions", - "path": "./.github/instructions/markdown.instructions.md", - "description": "Required instructions for creating or editing any Markdown (.md) files" - }, - { - "name": "prompt-builder-instructions", - "path": "./.github/instructions/prompt-builder.instructions.md", - "description": "Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core" - }, - { - "name": "python-script-instructions", - "path": "./.github/instructions/python-script.instructions.md", - "description": "Instructions for Python scripting implementation - Brought to you by microsoft/hve-core" - }, - { - "name": "terraform-instructions", - "path": "./.github/instructions/terraform/terraform.instructions.md", - "description": "Instructions for Terraform infrastructure as code implementation - Brought to you by microsoft/hve-core" - }, - { - "name": "uv-projects-instructions", - "path": "./.github/instructions/uv-projects.instructions.md", - "description": "Create and manage Python virtual environments using uv commands" - }, - { - "name": "writing-style-instructions", - "path": "./.github/instructions/writing-style.instructions.md", - "description": "Required writing style conventions for voice, tone, and language in all markdown content" - } - ] - }, + "contributes": {}, "author": "Microsoft", "license": "MIT" } diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index 0d0dbb9b..52e57dfc 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -461,51 +461,6 @@ function Resolve-RequiresDependencies { } } -function Get-FrontmatterData { - <# - .SYNOPSIS - Extracts description from YAML frontmatter. - .DESCRIPTION - Function that parses YAML frontmatter from a markdown file - and returns a hashtable with the description value. - .PARAMETER FilePath - Path to the markdown file to parse. - .PARAMETER FallbackDescription - Default description if none found in frontmatter. - .OUTPUTS - [hashtable] With description key. - #> - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter(Mandatory = $true)] - [string]$FilePath, - - [Parameter(Mandatory = $false)] - [string]$FallbackDescription = "" - ) - - $content = Get-Content -Path $FilePath -Raw - $description = "" - - if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') { - $yamlContent = $Matches[1] -replace '\r\n', "`n" -replace '\r', "`n" - try { - $data = ConvertFrom-Yaml -Yaml $yamlContent - if ($data.ContainsKey('description')) { - $description = $data.description - } - } - catch { - Write-Warning "Failed to parse YAML frontmatter in $(Split-Path -Leaf $FilePath): $_" - } - } - - return @{ - description = if ($description) { $description } else { $FallbackDescription } - } -} - function Test-PathsExist { <# .SYNOPSIS @@ -619,17 +574,14 @@ function Get-DiscoveredAgents { $maturity = $Registry.agents[$agentName].maturity } - $frontmatter = Get-FrontmatterData -FilePath $agentFile.FullName -FallbackDescription "AI agent for $agentName" - if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $agentName; Reason = "maturity: $maturity" } continue } $result.Agents += [PSCustomObject]@{ - name = $agentName - path = "./.github/agents/$($agentFile.Name)" - description = $frontmatter.description + name = $agentName + path = "./.github/agents/$($agentFile.Name)" } } @@ -685,16 +637,12 @@ function Get-DiscoveredPrompts { foreach ($promptFile in $promptFiles) { $promptName = $promptFile.BaseName -replace '\.prompt$', '' - $displayName = ($promptName -replace '-', ' ') -replace '(\b\w)', { $_.Groups[1].Value.ToUpper() } - # Determine maturity from registry if available, else default to stable $maturity = "stable" if ($Registry.Count -gt 0 -and $Registry.ContainsKey('prompts') -and $Registry.prompts.ContainsKey($promptName)) { $maturity = $Registry.prompts[$promptName].maturity } - $frontmatter = Get-FrontmatterData -FilePath $promptFile.FullName -FallbackDescription "Prompt for $displayName" - if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $promptName; Reason = "maturity: $maturity" } continue @@ -703,9 +651,8 @@ function Get-DiscoveredPrompts { $relativePath = [System.IO.Path]::GetRelativePath($GitHubDir, $promptFile.FullName) -replace '\\', '/' $result.Prompts += [PSCustomObject]@{ - name = $promptName - path = "./.github/$relativePath" - description = $frontmatter.description + name = $promptName + path = "./.github/$relativePath" } } @@ -768,7 +715,6 @@ function Get-DiscoveredInstructions { } $baseName = $instrFile.BaseName -replace '\.instructions$', '' $instrName = "$baseName-instructions" - $displayName = ($baseName -replace '-', ' ') -replace '(\b\w)', { $_.Groups[1].Value.ToUpper() } # Determine maturity from registry using relative path key $relPath = [System.IO.Path]::GetRelativePath($InstructionsDir, $instrFile.FullName) -replace '\\', '/' @@ -778,8 +724,6 @@ function Get-DiscoveredInstructions { $maturity = $Registry.instructions[$registryKey].maturity } - $frontmatter = Get-FrontmatterData -FilePath $instrFile.FullName -FallbackDescription "Instructions for $displayName" - if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $instrName; Reason = "maturity: $maturity" } continue @@ -789,9 +733,8 @@ function Get-DiscoveredInstructions { $normalizedRelativePath = (Join-Path ".github" $relativePathFromGitHub) -replace '\\', '/' $result.Instructions += [PSCustomObject]@{ - name = $instrName - path = "./$normalizedRelativePath" - description = $frontmatter.description + name = $instrName + path = "./$normalizedRelativePath" } } @@ -859,12 +802,9 @@ function Get-DiscoveredSkills { continue } - $frontmatter = Get-FrontmatterData -FilePath $skillFile -FallbackDescription "Skill for $skillName" - $result.Skills += [PSCustomObject]@{ - name = $skillName - path = "./.github/skills/$skillName" - description = $frontmatter.description + name = $skillName + path = "./.github/skills/$skillName" } } @@ -918,6 +858,12 @@ function Update-PackageJsonContributes { # Clone the object to avoid modifying the original $updated = $PackageJson | ConvertTo-Json -Depth 10 | ConvertFrom-Json + # Strip name and description; VS Code reads these from the files directly + $ChatAgents = @($ChatAgents | Select-Object -Property path) + $ChatPromptFiles = @($ChatPromptFiles | Select-Object -Property path) + $ChatInstructions = @($ChatInstructions | Select-Object -Property path) + $ChatSkills = @($ChatSkills | Select-Object -Property path) + # Ensure contributes section exists if (-not $updated.contributes) { $updated | Add-Member -NotePropertyName "contributes" -NotePropertyValue ([PSCustomObject]@{}) diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index d365da6c..0140917d 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -21,58 +21,6 @@ Describe 'Get-AllowedMaturities' { } -Describe 'Get-FrontmatterData' { - BeforeAll { - $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) - New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null - } - - AfterAll { - Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue - } - - It 'Extracts description from frontmatter' { - $testFile = Join-Path $script:tempDir 'test.md' - @' ---- -description: "Test description" ---- -# Content -'@ | Set-Content -Path $testFile - - $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'fallback' - $result.description | Should -Be 'Test description' - } - - It 'Returns hashtable with only description key' { - $testFile = Join-Path $script:tempDir 'desc-only.md' - @' ---- -description: "Desc" -maturity: preview ---- -# Content -'@ | Set-Content -Path $testFile - - $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'fallback' - $result.Keys | Should -Contain 'description' - $result.Keys | Should -Not -Contain 'maturity' - } - - It 'Uses fallback description when not in frontmatter' { - $testFile = Join-Path $script:tempDir 'no-desc.md' - @' ---- -applyTo: "**" ---- -# Content -'@ | Set-Content -Path $testFile - - $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'My Fallback' - $result.description | Should -Be 'My Fallback' - } -} - Describe 'Test-PathsExist' { BeforeAll { $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) From 7bca55ed7290fac2b6bd7551621b28fd948aba78 Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 08:56:56 +0100 Subject: [PATCH 20/62] refactor(scripts): streamline path handling in Copy-CollectionArtifacts function and update doc --- docs/contributing/ai-artifacts-common.md | 14 ++++++++------ scripts/extension/Package-Extension.ps1 | 14 +++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 9e932e92..97e7ddf2 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -214,12 +214,14 @@ Some artifacts require other artifacts to function correctly. The `requires` fie ### Dependency Types -| Type | Purpose | -|----------------|----------------------------------------------------------| -| `agents` | Agents this artifact delegates to or hands off to | -| `prompts` | Prompts this artifact invokes or references | -| `instructions` | Instructions this artifact relies on for code generation | -| `skills` | Skills this artifact executes for specialized tasks | +| Type | Purpose | +|----------------|----------------------------------------------------------------------------------| +| `agents` | Agents this artifact dispatches at runtime via `runSubagent` (excludes handoffs) | +| `prompts` | Prompts this artifact invokes or references | +| `instructions` | Instructions this artifact relies on for code generation | +| `skills` | Skills this artifact executes for specialized tasks | + +> **Note**: Frontmatter `handoffs` (UI buttons that suggest next agents) are resolved dynamically during packaging and MUST NOT be listed in `requires.agents`. Only agents invoked programmatically through `runSubagent` belong here. ### Declaring Dependencies diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index d40f2665..af76d1f2 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -542,7 +542,7 @@ function Copy-CollectionArtifacts { $agentsDestDir = Join-Path $ExtensionDirectory ".github/agents" New-Item -Path $agentsDestDir -ItemType Directory -Force | Out-Null foreach ($agent in $preparedPkgJson.contributes.chatAgents) { - $srcPath = Join-Path $RepoRoot ($agent.path.TrimStart('./')) + $srcPath = Join-Path $RepoRoot ($agent.path -replace '^\.[\\/]', '') if (Test-Path $srcPath) { Copy-Item -Path $srcPath -Destination $agentsDestDir -Force } @@ -552,8 +552,8 @@ function Copy-CollectionArtifacts { # Copy filtered prompts if ($preparedPkgJson.contributes.chatPromptFiles) { foreach ($prompt in $preparedPkgJson.contributes.chatPromptFiles) { - $srcPath = Join-Path $RepoRoot ($prompt.path.TrimStart('./')) - $destPath = Join-Path $ExtensionDirectory ($prompt.path.TrimStart('./')) + $srcPath = Join-Path $RepoRoot ($prompt.path -replace '^\.[\\/]', '') + $destPath = Join-Path $ExtensionDirectory ($prompt.path -replace '^\.[\\/]', '') $destDir = Split-Path $destPath -Parent New-Item -Path $destDir -ItemType Directory -Force | Out-Null if (Test-Path $srcPath) { @@ -565,8 +565,8 @@ function Copy-CollectionArtifacts { # Copy filtered instructions if ($preparedPkgJson.contributes.chatInstructions) { foreach ($instr in $preparedPkgJson.contributes.chatInstructions) { - $srcPath = Join-Path $RepoRoot ($instr.path.TrimStart('./')) - $destPath = Join-Path $ExtensionDirectory ($instr.path.TrimStart('./')) + $srcPath = Join-Path $RepoRoot ($instr.path -replace '^\.[\\/]', '') + $destPath = Join-Path $ExtensionDirectory ($instr.path -replace '^\.[\\/]', '') $destDir = Split-Path $destPath -Parent New-Item -Path $destDir -ItemType Directory -Force | Out-Null if (Test-Path $srcPath) { @@ -578,8 +578,8 @@ function Copy-CollectionArtifacts { # Copy filtered skills if ($preparedPkgJson.contributes.chatSkills) { foreach ($skill in $preparedPkgJson.contributes.chatSkills) { - $srcPath = Join-Path $RepoRoot ($skill.path.TrimStart('./')) - $destPath = Join-Path $ExtensionDirectory ($skill.path.TrimStart('./')) + $srcPath = Join-Path $RepoRoot ($skill.path -replace '^\.[\\/]', '') + $destPath = Join-Path $ExtensionDirectory ($skill.path -replace '^\.[\\/]', '') if (Test-Path $srcPath) { Copy-Item -Path $srcPath -Destination $destPath -Recurse -Force } From bedecaa082235f2eec19f6844287666d9b1516af Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 09:03:21 +0100 Subject: [PATCH 21/62] fix(agents): update github-issue-manager maturity status to deprecated refactor(docs): clarify agent dependency declaration in contributing guide refactor(scripts): ensure destination directories are created in Copy-CollectionArtifacts function --- .github/ai-artifacts-registry.json | 2 +- docs/contributing/custom-agents.md | 4 ++-- scripts/extension/Package-Extension.ps1 | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/ai-artifacts-registry.json b/.github/ai-artifacts-registry.json index 2b227e80..0edcfb61 100644 --- a/.github/ai-artifacts-registry.json +++ b/.github/ai-artifacts-registry.json @@ -192,7 +192,7 @@ } }, "github-issue-manager": { - "maturity": "stable", + "maturity": "deprecated", "personas": [ "hve-core-all" ], diff --git a/docs/contributing/custom-agents.md b/docs/contributing/custom-agents.md index 81d8044b..9e2964a8 100644 --- a/docs/contributing/custom-agents.md +++ b/docs/contributing/custom-agents.md @@ -237,11 +237,11 @@ Choose personas based on who benefits most from your agent: ### Declaring Agent Dependencies -If your agent delegates to other agents, invokes prompts, or generates code that follows specific instructions, declare these in the `requires` field: +If your agent dispatches other agents at runtime via `runSubagent`, invokes prompts, or generates code that follows specific instructions, declare these in the `requires` field. Handoff targets declared in frontmatter are resolved dynamically during packaging and should not be listed here: ```json "requires": { - "agents": ["task-planner"], // Agents this agent hands off to + "agents": ["task-planner"], // Agents dispatched at runtime via runSubagent "prompts": ["task-plan"], // Prompts this agent invokes "instructions": ["python-script"], // Instructions for generated code "skills": [] // Skills this agent executes diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index af76d1f2..c3aaa112 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -580,6 +580,8 @@ function Copy-CollectionArtifacts { foreach ($skill in $preparedPkgJson.contributes.chatSkills) { $srcPath = Join-Path $RepoRoot ($skill.path -replace '^\.[\\/]', '') $destPath = Join-Path $ExtensionDirectory ($skill.path -replace '^\.[\\/]', '') + $destDir = Split-Path $destPath -Parent + New-Item -Path $destDir -ItemType Directory -Force | Out-Null if (Test-Path $srcPath) { Copy-Item -Path $srcPath -Destination $destPath -Recurse -Force } From f5139c2416787074b9ec58765936b93b185bfd1e Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 09:15:37 +0100 Subject: [PATCH 22/62] feat(scripts): add per-artifact and JSON Schema validation to registry linter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add Test-ArtifactEntries for maturity enum, personas/tags, and additionalProperties checks - add Test-JsonSchemaValidation using Test-Json -SchemaFile (PowerShell 7.4+) - enforce additionalProperties constraints in Test-RegistryStructure and Test-PersonaReferences - add Pester tests and fixtures for all new validation functions ๐Ÿ” - Generated by Copilot --- scripts/linting/Validate-ArtifactRegistry.ps1 | 195 ++++++++++++- .../extra-persona-properties.json | 18 ++ .../ArtifactRegistry/extra-properties.json | 41 +++ .../ArtifactRegistry/invalid-maturity.json | 26 ++ .../invalid-persona-format-in-artifact.json | 27 ++ .../missing-artifact-fields.json | 26 ++ .../Validate-ArtifactRegistry.Tests.ps1 | 273 ++++++++++++++++++ 7 files changed, 602 insertions(+), 4 deletions(-) create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/extra-persona-properties.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-format-in-artifact.json create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/missing-artifact-fields.json diff --git a/scripts/linting/Validate-ArtifactRegistry.ps1 b/scripts/linting/Validate-ArtifactRegistry.ps1 index 8f9b108c..de954b4e 100644 --- a/scripts/linting/Validate-ArtifactRegistry.ps1 +++ b/scripts/linting/Validate-ArtifactRegistry.ps1 @@ -14,7 +14,10 @@ .DESCRIPTION Validates the `.github/ai-artifacts-registry.json` file by checking: - - JSON structure and required fields + - JSON structure, required fields, and additional-property constraints + - JSON Schema validation (PowerShell 7.4+ with Test-Json -SchemaFile) + - Per-artifact required fields (maturity, personas, tags) + - Maturity enum values and persona reference format - Persona ID format and reference validity - Artifact file existence on disk - Dependency reference validity @@ -107,6 +110,23 @@ function Test-RegistryStructure { $errors.Add("Missing required field: personas.definitions") } + # Additional properties - top level + $allowedTopLevel = @('$schema', 'version', 'personas', 'agents', 'prompts', 'instructions', 'skills') + foreach ($key in $registry.Keys) { + if ($key -notin $allowedTopLevel) { + $errors.Add("Unexpected top-level property: $key") + } + } + + # Additional properties - personas + if ($registry.ContainsKey('personas')) { + foreach ($key in $registry['personas'].Keys) { + if ($key -ne 'definitions') { + $errors.Add("Unexpected property in personas: $key") + } + } + } + return @{ Success = ($errors.Count -eq 0) Errors = $errors @@ -141,6 +161,13 @@ function Test-PersonaReferences { if (-not $persona.ContainsKey('description') -or [string]::IsNullOrEmpty($persona['description'])) { $errors.Add("Persona '$personaId' missing 'description' field") } + # Additional properties on persona definitions + $allowedPersonaProps = @('name', 'description') + foreach ($prop in $persona.Keys) { + if ($prop -notin $allowedPersonaProps) { + $errors.Add("Persona '$personaId' has unexpected property: $prop") + } + } } # Validate persona references in artifacts @@ -161,6 +188,157 @@ function Test-PersonaReferences { return @{ Success = ($errors.Count -eq 0); Errors = $errors } } +function Test-ArtifactEntries { + <# + .SYNOPSIS + Validates per-artifact required fields, maturity enum, and property constraints. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory)] + [hashtable]$Registry + ) + + $errors = [System.Collections.Generic.List[string]]::new() + $validMaturity = @('stable', 'preview', 'experimental', 'deprecated') + $personaPattern = '^[a-z][a-z0-9-]*$' + + $simpleAllowed = @('maturity', 'personas', 'tags') + $agentAllowed = @('maturity', 'personas', 'tags', 'requires') + $requiresAllowed = @('agents', 'prompts', 'instructions', 'skills') + + $sectionAllowed = @{ + agents = $agentAllowed + prompts = $simpleAllowed + instructions = $simpleAllowed + skills = $simpleAllowed + } + + foreach ($section in $sectionAllowed.Keys) { + if (-not $Registry.ContainsKey($section)) { continue } + + foreach ($key in $Registry[$section].Keys) { + $entry = $Registry[$section][$key] + $prefix = "${section}/${key}" + + if ($entry -isnot [hashtable]) { + $errors.Add("${prefix}: entry must be an object") + continue + } + + # Required field: maturity + if (-not $entry.ContainsKey('maturity')) { + $errors.Add("${prefix}: missing required field 'maturity'") + } + elseif ($entry['maturity'] -notin $validMaturity) { + $errors.Add("${prefix}: invalid maturity '$($entry['maturity'])'. Must be one of: $($validMaturity -join ', ')") + } + + # Required field: personas + if (-not $entry.ContainsKey('personas')) { + $errors.Add("${prefix}: missing required field 'personas'") + } + elseif ($entry['personas'] -isnot [System.Collections.IList]) { + $errors.Add("${prefix}: 'personas' must be an array") + } + else { + foreach ($p in $entry['personas']) { + if ($p -notmatch $personaPattern) { + $errors.Add("${prefix}: invalid persona reference format '$p'. Must match: $personaPattern") + } + } + } + + # Required field: tags + if (-not $entry.ContainsKey('tags')) { + $errors.Add("${prefix}: missing required field 'tags'") + } + elseif ($entry['tags'] -isnot [System.Collections.IList]) { + $errors.Add("${prefix}: 'tags' must be an array") + } + + # No unexpected properties + $allowed = $sectionAllowed[$section] + foreach ($prop in $entry.Keys) { + if ($prop -notin $allowed) { + $errors.Add("${prefix}: unexpected property '$prop'") + } + } + + # Agent requires block structure + if ($section -eq 'agents' -and $entry.ContainsKey('requires')) { + $requires = $entry['requires'] + if ($requires -isnot [hashtable]) { + $errors.Add("${prefix}: 'requires' must be an object") + } + else { + foreach ($reqProp in $requires.Keys) { + if ($reqProp -notin $requiresAllowed) { + $errors.Add("${prefix}: unexpected property in requires: '$reqProp'") + } + elseif ($requires[$reqProp] -isnot [System.Collections.IList]) { + $errors.Add("${prefix}: requires.$reqProp must be an array") + } + } + } + } + } + } + + return @{ Success = ($errors.Count -eq 0); Errors = $errors } +} + +function Test-JsonSchemaValidation { + <# + .SYNOPSIS + Validates registry JSON against the schema file using Test-Json when available. + .DESCRIPTION + Requires PowerShell 7.4+ for Test-Json -SchemaFile support. Skips gracefully + on older versions. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory)] + [string]$RegistryPath, + + [Parameter(Mandatory)] + [string]$SchemaPath + ) + + $errors = [System.Collections.Generic.List[string]]::new() + + if (-not (Test-Path $SchemaPath)) { + $errors.Add("Schema file not found: $SchemaPath") + return @{ Success = $false; Errors = $errors } + } + + if ($PSVersionTable.PSVersion -lt [version]'7.4') { + Write-Verbose "Skipping JSON Schema validation: requires PowerShell 7.4+ (current: $($PSVersionTable.PSVersion))" + return @{ Success = $true; Errors = $errors } + } + + try { + $content = Get-Content -Path $RegistryPath -Raw + $schemaErrors = $null + $valid = Test-Json -Json $content -SchemaFile $SchemaPath -ErrorAction SilentlyContinue -ErrorVariable schemaErrors + if (-not $valid) { + foreach ($schemaErr in $schemaErrors) { + $errors.Add("Schema violation: $schemaErr") + } + if ($errors.Count -eq 0) { + $errors.Add("Registry does not conform to JSON Schema") + } + } + } + catch { + $errors.Add("JSON Schema validation error: $($_.Exception.Message)") + } + + return @{ Success = ($errors.Count -eq 0); Errors = $errors } +} + function Get-ArtifactPath { <# .SYNOPSIS @@ -565,16 +743,25 @@ try { $personaResult = Test-PersonaReferences -Registry $registry $allErrors.AddRange($personaResult.Errors) - # Step 3: File existence + # Step 3: Artifact entry validation (maturity, personas, tags, additionalProperties) + $entryResult = Test-ArtifactEntries -Registry $registry + $allErrors.AddRange($entryResult.Errors) + + # Step 4: JSON Schema validation (PowerShell 7.4+) + $schemaPath = Join-Path $PSScriptRoot 'schemas/ai-artifacts-registry.schema.json' + $schemaResult = Test-JsonSchemaValidation -RegistryPath $RegistryPath -SchemaPath $schemaPath + $allErrors.AddRange($schemaResult.Errors) + + # Step 5: File existence $fileResult = Test-ArtifactFileExistence -Registry $registry -RepoRoot $RepoRoot $allErrors.AddRange($fileResult.Errors) - # Step 4: Dependency references + # Step 6: Dependency references $depResult = Test-DependencyReferences -Registry $registry $allErrors.AddRange($depResult.Errors) $allWarnings.AddRange($depResult.Warnings) - # Step 5: Orphan detection + # Step 7: Orphan detection $orphanResult = Find-OrphanArtifacts -Registry $registry -RepoRoot $RepoRoot $allWarnings.AddRange($orphanResult.Warnings) diff --git a/scripts/tests/Fixtures/ArtifactRegistry/extra-persona-properties.json b/scripts/tests/Fixtures/ArtifactRegistry/extra-persona-properties.json new file mode 100644 index 00000000..12e1ea8f --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/extra-persona-properties.json @@ -0,0 +1,18 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "developer": { + "name": "Developer", + "description": "Software engineers", + "extraField": "not-allowed" + } + }, + "extraSection": "not-allowed" + }, + "agents": {}, + "prompts": {}, + "instructions": {}, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json b/scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json new file mode 100644 index 00000000..6d7e960f --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json @@ -0,0 +1,41 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "developer": { + "name": "Developer", + "description": "Software engineers" + } + } + }, + "agents": { + "extra-agent": { + "maturity": "stable", + "personas": [ + "developer" + ], + "tags": [ + "test" + ], + "customField": true + } + }, + "prompts": { + "extra-prompt": { + "maturity": "stable", + "personas": [ + "developer" + ], + "tags": [ + "test" + ], + "requires": { + "agents": [] + } + } + }, + "instructions": {}, + "skills": {}, + "extraTopLevel": "should-not-be-here" +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json new file mode 100644 index 00000000..6af34425 --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json @@ -0,0 +1,26 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "developer": { + "name": "Developer", + "description": "Software engineers" + } + } + }, + "agents": { + "bad-agent": { + "maturity": "INVALID", + "personas": [ + "developer" + ], + "tags": [ + "test" + ] + } + }, + "prompts": {}, + "instructions": {}, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-format-in-artifact.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-format-in-artifact.json new file mode 100644 index 00000000..d93a68a8 --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-format-in-artifact.json @@ -0,0 +1,27 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "developer": { + "name": "Developer", + "description": "Software engineers" + } + } + }, + "agents": { + "bad-persona-ref": { + "maturity": "stable", + "personas": [ + "Bad Format!", + "123-invalid" + ], + "tags": [ + "test" + ] + } + }, + "prompts": {}, + "instructions": {}, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/missing-artifact-fields.json b/scripts/tests/Fixtures/ArtifactRegistry/missing-artifact-fields.json new file mode 100644 index 00000000..13719260 --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/missing-artifact-fields.json @@ -0,0 +1,26 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "developer": { + "name": "Developer", + "description": "Software engineers" + } + } + }, + "agents": {}, + "prompts": { + "bad-prompt": { + "tags": [ + "test" + ] + } + }, + "instructions": { + "bad-instruction": { + "maturity": "stable" + } + }, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 index 3270a1aa..caf02cf7 100644 --- a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 +++ b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 @@ -27,6 +27,12 @@ BeforeAll { $script:UnknownDepRefsPath = Join-Path $script:FixtureDir 'unknown-dep-refs.json' $script:CircularDepsPath = Join-Path $script:FixtureDir 'circular-deps.json' $script:NoRequiresPath = Join-Path $script:FixtureDir 'no-requires.json' + $script:InvalidMaturityPath = Join-Path $script:FixtureDir 'invalid-maturity.json' + $script:MissingArtifactFieldsPath = Join-Path $script:FixtureDir 'missing-artifact-fields.json' + $script:ExtraPropertiesPath = Join-Path $script:FixtureDir 'extra-properties.json' + $script:InvalidPersonaFormatInArtifactPath = Join-Path $script:FixtureDir 'invalid-persona-format-in-artifact.json' + $script:ExtraPersonaPropertiesPath = Join-Path $script:FixtureDir 'extra-persona-properties.json' + $script:SchemaPath = Join-Path $PSScriptRoot '../../linting/schemas/ai-artifacts-registry.schema.json' } #region Test-RegistryStructure Tests @@ -79,6 +85,23 @@ Describe 'Test-RegistryStructure' -Tag 'Unit' { $result.Errors | Should -Contain 'Missing required field: personas.definitions' } } + + Context 'Additional properties validation' { + It 'Reports unexpected top-level properties' { + $result = Test-RegistryStructure -RegistryPath $script:ExtraPropertiesPath + $result.Errors | Where-Object { $_ -match 'Unexpected top-level property: extraTopLevel' } | Should -Not -BeNullOrEmpty + } + + It 'Reports unexpected personas sub-properties' { + $result = Test-RegistryStructure -RegistryPath $script:ExtraPersonaPropertiesPath + $result.Errors | Where-Object { $_ -match 'Unexpected property in personas: extraSection' } | Should -Not -BeNullOrEmpty + } + + It 'Does not report errors for valid top-level properties' { + $result = Test-RegistryStructure -RegistryPath $script:ValidRegistryPath + $result.Errors | Where-Object { $_ -match 'Unexpected' } | Should -BeNullOrEmpty + } + } } #endregion @@ -138,6 +161,244 @@ Describe 'Test-PersonaReferences' -Tag 'Unit' { $result.Errors | Where-Object { $_ -match 'references undefined persona' } | Should -Not -BeNullOrEmpty } } + + Context 'Persona definition additional properties' { + It 'Reports unexpected properties on persona definitions' { + $content = Get-Content $script:ExtraPersonaPropertiesPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-PersonaReferences -Registry $registry + $result.Errors | Where-Object { $_ -match "has unexpected property: extraField" } | Should -Not -BeNullOrEmpty + } + + It 'Does not report errors for valid persona definitions' { + $result = Test-PersonaReferences -Registry $script:ValidRegistry + $result.Errors | Where-Object { $_ -match 'unexpected property' } | Should -BeNullOrEmpty + } + } +} + +#endregion + +#region Test-ArtifactEntries Tests + +Describe 'Test-ArtifactEntries' -Tag 'Unit' { + BeforeAll { + $content = Get-Content $script:ValidRegistryPath -Raw + $script:ValidRegistry = $content | ConvertFrom-Json -AsHashtable + } + + Context 'Valid registry' { + It 'Passes with fully valid registry' { + $result = Test-ArtifactEntries -Registry $script:ValidRegistry + $result.Success | Should -BeTrue + $result.Errors.Count | Should -Be 0 + } + } + + Context 'Maturity validation' { + It 'Reports invalid maturity value' { + $content = Get-Content $script:InvalidMaturityPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match "invalid maturity 'INVALID'" } | Should -Not -BeNullOrEmpty + } + + It 'Accepts all valid maturity values' { + foreach ($maturity in @('stable', 'preview', 'experimental', 'deprecated')) { + $registry = @{ + agents = @{} + prompts = @{ + 'test' = @{ maturity = $maturity; personas = @(); tags = @() } + } + instructions = @{} + skills = @{} + } + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match 'maturity' } | Should -BeNullOrEmpty + } + } + } + + Context 'Required artifact fields' { + It 'Reports missing maturity field' { + $content = Get-Content $script:MissingArtifactFieldsPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match "missing required field 'maturity'" } | Should -Not -BeNullOrEmpty + } + + It 'Reports missing personas field' { + $content = Get-Content $script:MissingArtifactFieldsPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match "missing required field 'personas'" } | Should -Not -BeNullOrEmpty + } + + It 'Reports missing tags field' { + $registry = @{ + agents = @{} + prompts = @{ + 'no-tags' = @{ maturity = 'stable'; personas = @() } + } + instructions = @{} + skills = @{} + } + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match "missing required field 'tags'" } | Should -Not -BeNullOrEmpty + } + } + + Context 'Persona reference format in artifacts' { + It 'Reports invalid persona reference format' { + $content = Get-Content $script:InvalidPersonaFormatInArtifactPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match "invalid persona reference format 'Bad Format!'" } | Should -Not -BeNullOrEmpty + $result.Errors | Where-Object { $_ -match "invalid persona reference format '123-invalid'" } | Should -Not -BeNullOrEmpty + } + + It 'Accepts valid persona reference format' { + $registry = @{ + agents = @{} + prompts = @{ + 'good' = @{ maturity = 'stable'; personas = @('developer', 'dev-ops-2'); tags = @() } + } + instructions = @{} + skills = @{} + } + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match 'persona reference format' } | Should -BeNullOrEmpty + } + } + + Context 'Additional properties on artifacts' { + It 'Reports unexpected properties on agent entries' { + $content = Get-Content $script:ExtraPropertiesPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match "unexpected property 'customField'" } | Should -Not -BeNullOrEmpty + } + + It 'Reports requires block on non-agent sections' { + $content = Get-Content $script:ExtraPropertiesPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match "prompts/extra-prompt.*unexpected property 'requires'" } | Should -Not -BeNullOrEmpty + } + + It 'Allows requires block on agent entries' { + $result = Test-ArtifactEntries -Registry $script:ValidRegistry + $result.Errors | Where-Object { $_ -match "unexpected property 'requires'" } | Should -BeNullOrEmpty + } + } + + Context 'Agent requires block structure' { + It 'Reports unexpected properties in requires block' { + $registry = @{ + agents = @{ + 'bad-requires' = @{ + maturity = 'stable' + personas = @() + tags = @() + requires = @{ agents = @(); unknownRef = @() } + } + } + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match "unexpected property in requires: 'unknownRef'" } | Should -Not -BeNullOrEmpty + } + + It 'Reports non-array values in requires block' { + $registry = @{ + agents = @{ + 'bad-requires' = @{ + maturity = 'stable' + personas = @() + tags = @() + requires = @{ agents = 'not-an-array' } + } + } + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match 'requires.agents must be an array' } | Should -Not -BeNullOrEmpty + } + } + + Context 'Type validation' { + It 'Reports non-array personas value' { + $registry = @{ + agents = @{} + prompts = @{ + 'bad-type' = @{ maturity = 'stable'; personas = 'not-array'; tags = @() } + } + instructions = @{} + skills = @{} + } + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match "'personas' must be an array" } | Should -Not -BeNullOrEmpty + } + + It 'Reports non-array tags value' { + $registry = @{ + agents = @{} + prompts = @{ + 'bad-type' = @{ maturity = 'stable'; personas = @(); tags = 'not-array' } + } + instructions = @{} + skills = @{} + } + $result = Test-ArtifactEntries -Registry $registry + $result.Errors | Where-Object { $_ -match "'tags' must be an array" } | Should -Not -BeNullOrEmpty + } + } +} + +#endregion + +#region Test-JsonSchemaValidation Tests + +Describe 'Test-JsonSchemaValidation' -Tag 'Unit' { + Context 'Schema file existence' { + It 'Reports error when schema file does not exist' { + $result = Test-JsonSchemaValidation -RegistryPath $script:ValidRegistryPath -SchemaPath '/nonexistent/schema.json' + $result.Success | Should -BeFalse + $result.Errors[0] | Should -Match 'Schema file not found' + } + } + + Context 'Valid registry against schema' { + It 'Passes with valid registry' { + $result = Test-JsonSchemaValidation -RegistryPath $script:ValidRegistryPath -SchemaPath $script:SchemaPath + $result.Success | Should -BeTrue + $result.Errors.Count | Should -Be 0 + } + } + + Context 'Invalid registry against schema' { + It 'Reports schema violations for invalid maturity' { + $result = Test-JsonSchemaValidation -RegistryPath $script:InvalidMaturityPath -SchemaPath $script:SchemaPath + $result.Success | Should -BeFalse + $result.Errors.Count | Should -BeGreaterThan 0 + } + + It 'Reports schema violations for missing required artifact fields' { + $result = Test-JsonSchemaValidation -RegistryPath $script:MissingArtifactFieldsPath -SchemaPath $script:SchemaPath + $result.Success | Should -BeFalse + $result.Errors.Count | Should -BeGreaterThan 0 + } + + It 'Reports schema violations for extra properties' { + $result = Test-JsonSchemaValidation -RegistryPath $script:ExtraPropertiesPath -SchemaPath $script:SchemaPath + $result.Success | Should -BeFalse + $result.Errors.Count | Should -BeGreaterThan 0 + } + } } #endregion @@ -984,6 +1245,18 @@ Describe 'Edge Cases' -Tag 'Unit' { $result.Success | Should -BeTrue } + It 'Test-ArtifactEntries succeeds with empty sections' { + $registry = @{ + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + $result = Test-ArtifactEntries -Registry $registry + $result.Success | Should -BeTrue + $result.Errors.Count | Should -Be 0 + } + It 'Handles empty personas definitions' { $registry = @{ personas = @{ definitions = @{} } From fbb6662623238d9fee03246b9e0bdc50f0705fbb Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 09:28:46 +0100 Subject: [PATCH 23/62] feat(agents): add persona-based agent filtering to installer - introduce option to install agents by persona collection - update user input handling for persona selection - enhance documentation on persona filtering process --- .github/agents/hve-core-installer.agent.md | 94 ++++++++++++++++++++-- docs/getting-started/install.md | 8 +- 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/.github/agents/hve-core-installer.agent.md b/.github/agents/hve-core-installer.agent.md index 0b0ed2c4..401884e1 100644 --- a/.github/agents/hve-core-installer.agent.md +++ b/.github/agents/hve-core-installer.agent.md @@ -1065,26 +1065,100 @@ Copying agents enables local customization and offline use. Options: [1] Install all agents (recommended) - [2] Install RPI Core only - [3] Skip agent installation + [2] Install by persona collection + [3] Install RPI Core only + [4] Skip agent installation -Your choice? (1/2/3) +Your choice? (1/2/3/4) ``` User input handling: -* "1", "all", "install all" โ†’ Copy all agents -* "2", "rpi", "rpi core", "core" โ†’ Copy RPI Core bundle only -* "3", "skip", "none", "no" โ†’ Skip to success report +* "1", "all", "install all" โ†’ Copy all stable agents +* "2", "persona", "collection", "by persona" โ†’ Proceed to Persona Selection sub-flow +* "3", "rpi", "rpi core", "core" โ†’ Copy RPI Core bundle only +* "4", "skip", "none", "no" โ†’ Skip to success report * Unclear response โ†’ Ask for clarification +### Persona Selection Sub-Flow + +When the user selects option 2, read the artifact registry to present available personas. + +**Step 1: Read registry and build persona agent counts** + +Read `.github/ai-artifacts-registry.json` from the HVE-Core source (at `$hveCoreBasePath`). Parse `personas.definitions` for display names and descriptions. For each agent entry, count stable agents per persona (exclude `experimental` and `deprecated` maturity). + +**Step 2: Present persona options** + + +```text +๐ŸŽญ Persona Collection Selection + +Choose one or more personas to install agents tailored to your role. +Agents marked for all personas are always included. + +| # | Persona | Agents | Description | +|---|--------------------|--------|----------------------------------| +| 1 | Developer | [N] | Software engineers writing code | +| 2 | TPM | [N] | Program/product managers | +| 3 | DevOps Engineer | [N] | Platform, SRE, infrastructure | +| 4 | Architect | [N] | Solution/system architects | +| 5 | Technical Writer | [N] | Documentation specialists | + +Enter persona number(s) separated by commas (e.g., "1, 3"): +``` + + +Agent counts `[N]` include agents matching the persona plus all `hve-core-all` agents with `stable` maturity. + +User input handling: + +* Single number (e.g., "1") โ†’ Select that persona +* Multiple numbers (e.g., "1, 3") โ†’ Combine agent sets from selected personas +* Persona name (e.g., "developer") โ†’ Match by identifier +* Unclear response โ†’ Ask for clarification + +**Step 3: Build filtered agent list** + +For each selected persona identifier: + +1. Iterate through `agents` in the registry +2. Include agents where `maturity` is `stable` AND `personas` array contains the selected persona identifier OR `hve-core-all` +3. Deduplicate across multiple selected personas + +**Step 4: Present filtered agents for confirmation** + + +```text +๐Ÿ“‹ Agents for [Persona Name(s)] + +The following [N] agents will be copied: + + โ€ข [agent-name-1] - [agent description from registry tags] + โ€ข [agent-name-2] - [agent description from registry tags] + ... + +Proceed with installation? (yes/no) +``` + + +User input handling: + +* "yes", "y" โ†’ Proceed with copy using filtered agent list +* "no", "n" โ†’ Return to Checkpoint 6 for re-selection +* Unclear response โ†’ Ask for clarification + +> [!NOTE] +> Persona filtering applies to agents only. Copying of related prompts, instructions, and skills based on persona is planned for a future release. + ### Agent Bundle Definitions | Bundle | Agents | | ------ | ------ | | `rpi-core` | task-researcher, task-planner, task-implementor, rpi-agent | -| `all` | All 20 agents (see prompt for full list) | +| `all` | All stable agents from the registry | +| `persona:` | Stable agents matching the persona plus all `hve-core-all` agents | ### Collision Detection @@ -1106,6 +1180,10 @@ $targetDir = ".github/agents" $filesToCopy = switch ($selection) { "rpi-core" { @("task-researcher.agent.md", "task-planner.agent.md", "task-implementor.agent.md", "rpi-agent.agent.md") } "all" { Get-ChildItem "$sourceDir/*.agent.md" | ForEach-Object { $_.Name } } + default { + # Persona-based: $selection contains filtered agent names from registry + $personaAgents + } } # Check for collisions @@ -1187,6 +1265,7 @@ $manifest = @{ source = "microsoft/hve-core" version = (Get-Content "$hveCoreBasePath/package.json" | ConvertFrom-Json).version installed = (Get-Date -Format "o") + collection = $collectionId # "hve-core-all", "rpi-core", or persona id(s) e.g. "developer" or "developer,devops" files = @{}; skip = @() } @@ -1263,6 +1342,7 @@ if (Test-Path $manifestPath) { Write-Host "INSTALLED_VERSION=$($manifest.version)" Write-Host "SOURCE_VERSION=$sourceVersion" Write-Host "VERSION_CHANGED=$($sourceVersion -ne $manifest.version)" + Write-Host "INSTALLED_COLLECTION=$($manifest.collection ?? 'hve-core-all')" } else { Write-Host "UPGRADE_MODE=false" } diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index de662a6c..23917a41 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -100,16 +100,16 @@ The VS Code Marketplace extension installs the **full collection** containing al ### Clone Methods (Persona Filtering) -Clone-based installation methods support persona filtering through the installer agent: +Clone-based installation methods support persona-based agent filtering through the installer agent: 1. Clone the repository using your preferred method 2. Run the `hve-core-installer` agent -3. In Phase 7 (Agent Customization), select your role-based collection +3. In Phase 7 (Agent Customization), select your role-based collection or install all agents -The installer filters artifacts based on your selected persona, copying only relevant agents, prompts, and instructions to your project. +The installer reads persona assignments from the artifact registry and copies only the agents assigned to your selected persona. Agents marked for all personas are always included. > [!NOTE] -> Persona filtering requires the artifact registry (`.github/ai-artifacts-registry.json`). The installer agent uses this registry to determine which artifacts belong to each collection. +> Persona filtering applies to agents only. Copying of related prompts, instructions, and skills based on persona is planned for a future release. ### Quick Decision Tree From 167bfa731f962b89401e6b514786c68ad7e246e4 Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 09:49:09 +0100 Subject: [PATCH 24/62] docs: clarify maturity filtering for artifacts and handoffs - update documentation on maturity filtering rules for and - enhance clarity on artifact inclusion criteria during packaging - reference relevant sections in the documentation for better understanding --- docs/contributing/ai-artifacts-common.md | 13 +++++++++++++ extension/PACKAGING.md | 8 ++++---- scripts/extension/Prepare-Extension.ps1 | 5 ++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 97e7ddf2..345210dc 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -223,6 +223,19 @@ Some artifacts require other artifacts to function correctly. The `requires` fie > **Note**: Frontmatter `handoffs` (UI buttons that suggest next agents) are resolved dynamically during packaging and MUST NOT be listed in `requires.agents`. Only agents invoked programmatically through `runSubagent` belong here. +### Handoff vs Requires Maturity Filtering + +Handoff targets and `requires` dependencies follow different maturity rules during extension packaging: + +| Mechanism | Maturity Filtered | Reason | +| ---------- | ----------------- | ------------------------------------------------------------------------- | +| `requires` | Yes | Runtime dependencies are excluded when their maturity exceeds the channel | +| `handoffs` | No | UI buttons must resolve to a valid agent or the button is broken | + +During extension packaging (`scripts/extension/Prepare-Extension.ps1`), the `Resolve-HandoffDependencies` function encounters a handoff target whose maturity falls outside the allowed set and still includes that agent in the package. The maturity check only gates whether the target's own handoffs are traversed further. This ensures that a stable agent handing off to a preview agent produces a functional UI button in both stable and pre-release channels. + +The companion function `Resolve-RequiresDependencies` in the same script applies strict maturity filtering: dependencies whose maturity level is outside the allowed set are excluded entirely. + ### Declaring Dependencies Add the `requires` field to artifacts that depend on others: diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index b4949519..78510490 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -260,7 +260,7 @@ The workflow validates the version is ODD before proceeding. ### Agent Maturity Filtering -When packaging, agents are filtered by their `maturity` frontmatter field: +When packaging, artifacts are filtered by their `maturity` field in `.github/ai-artifacts-registry.json`: | Channel | Included Maturity Levels | |------------|-------------------------------------| @@ -348,8 +348,8 @@ When building a collection, the system: 1. Reads the collection manifest to get the target personas 2. Reads the artifact registry (`.github/ai-artifacts-registry.json`) -3. Includes artifacts where `personas` array contains any of the collection's personas -4. Includes all `hve-core-all` artifacts as the base set +3. Includes artifacts whose `personas` array contains any of the collection's personas +4. Includes artifacts with an empty `personas` array (universal artifacts shared across all collections) 5. Resolves artifact dependencies to ensure completeness ### Testing Collection Builds Locally @@ -358,7 +358,7 @@ To verify artifact inclusion before publishing: ```bash # 1. Prepare with collection filtering -pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection developer -Verbose +pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection extension/collections/developer.collection.json -Verbose # 2. Check package.json for included artifacts cat extension/package.json | jq '.contributes.chatAgents' diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index 52e57dfc..f1431bb0 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -337,7 +337,10 @@ function Resolve-HandoffDependencies { $data = ConvertFrom-Yaml -Yaml $yamlContent if ($data.ContainsKey('handoffs') -and $data.handoffs -is [System.Collections.IEnumerable] -and $data.handoffs -isnot [string]) { foreach ($handoff in $data.handoffs) { - # Handle both string format and object format (with 'agent' field) + # Handle both string format and object format (with 'agent' field). + # Handoff targets bypass maturity filtering by design. + # See docs/contributing/ai-artifacts-common.md + # "Handoff vs Requires Maturity Filtering" for rationale. $targetAgent = $null if ($handoff -is [string]) { $targetAgent = $handoff From a51255f4072d240562af3ab6f17ff55176a26f41 Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 10:15:22 +0100 Subject: [PATCH 25/62] docs(extension): update PACKAGING.md with packaging pipeline overview and artifact discovery details --- extension/PACKAGING.md | 123 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 112 insertions(+), 11 deletions(-) diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index 78510490..4c066734 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -2,7 +2,7 @@ title: Extension Packaging Guide description: Developer guide for packaging and publishing the HVE Core VS Code extension author: Microsoft -ms.date: 2026-02-06 +ms.date: 2026-02-10 ms.topic: reference --- @@ -60,6 +60,61 @@ The extension is automatically packaged and published through GitHub Actions: | `.github/workflows/extension-publish.yml` | Release/manual | Publishes to VS Code Marketplace | | `.github/workflows/main.yml` | Push to main | Includes extension packaging in CI | +## Packaging Pipeline Overview + +Extension packaging is a two-step process: **Prepare** discovers and filters artifacts into `package.json`, then **Package** copies files, runs `vsce`, and cleans up. + +```mermaid +flowchart LR + subgraph Prepare["Step 1 ยท Prepare-Extension.ps1"] + P1[Load Registry] --> P2[Discover Artifacts] + P2 --> P3["Filter by Maturity
+ Collection"] + P3 --> P4[Resolve Dependencies] + P4 --> P5[Write package.json] + end + + subgraph Package["Step 2 ยท Package-Extension.ps1"] + K1[Resolve Version] --> K2["Copy Assets
to extension/"] + K2 --> K3[vsce package] + K3 --> K4[Cleanup & Restore] + end + + Prepare --> Package --> VSIX[".vsix"] +``` + +### Artifact Discovery and Resolution + +The prepare step discovers all artifact files on disk, filters them through the registry, and optionally narrows the set to a specific persona collection. Dependency resolution ensures transitive handoff and requires references pull in all needed artifacts. + +```mermaid +flowchart TB + REG[ai-artifacts-registry.json] -->|Get-RegistryData| INPUTS + CH[Channel: Stable / PreRelease] -->|Get-AllowedMaturities| INPUTS + CM["Collection Manifest
optional"] -->|Get-CollectionManifest| INPUTS + + INPUTS[Resolve Inputs] --> DISC[Discover Artifact Files from .github/] + + DISC --> AG["Agents
.github/agents/*.agent.md"] + DISC --> PR["Prompts
.github/prompts/*.prompt.md"] + DISC --> IN["Instructions
.github/instructions/*.instructions.md"] + DISC --> SK["Skills
.github/skills/*/SKILL.md"] + + AG -->|Filter by maturity| FM[Maturity-Filtered Set] + PR -->|Filter by maturity| FM + IN -->|"Filter by maturity
+ exclude hve-core/"| FM + SK -->|Filter by maturity| FM + + FM --> CF{"Collection
specified?"} + CF -->|No| FINAL[Final Artifact Set] + CF -->|Yes| PA["Filter by persona + globs
Get-CollectionArtifacts"] + PA --> HD["Resolve Handoff Closure
BFS through agent frontmatter"] + HD --> RD["Resolve Requires Dependencies
BFS through registry requires"] + RD --> INT["Intersect with
discovered artifacts"] + INT --> FINAL + + FINAL --> UPD["Update package.json contributes
chatAgents ยท chatPromptFiles
chatInstructions ยท chatSkills"] +``` + ## Packaging the Extension ### Using the Automated Scripts (Recommended) @@ -121,11 +176,36 @@ The packaging script automatically: * Uses version from `package.json` (or specified version) * Optionally appends dev patch number for pre-release builds -* Copies required `.github` directory -* Copies `scripts/dev-tools` directory (developer utilities) +* Copies required directories into `extension/` (or only filtered artifacts in collection mode) * Packages the extension using `vsce` -* Cleans up temporary files -* Restores original `package.json` version if temporarily modified +* Cleans up temporary files and restores all modified files + +```mermaid +flowchart TB + PKG["package.json"] -->|"Read & validate"| VER[Resolve Version] + VER --> TMPVER{"Version
changed?"} + TMPVER -->|Yes| WRITE["Temporarily update
package.json version"] + TMPVER -->|No| PREP + WRITE --> PREP[Prepare Extension Directory] + + PREP --> MODE{"Collection
mode?"} + MODE -->|"Full (default)"| FULL["Copy entire .github/
+ scripts/dev-tools/
+ scripts/lib/Modules/CIHelpers.psm1
+ docs/templates/
+ .github/skills/"] + MODE -->|Collection| COLL["Copy only artifacts listed
in package.json contributes
+ scripts/dev-tools/
+ scripts/lib/Modules/CIHelpers.psm1
+ docs/templates/"] + + FULL --> RDM{"Persona
README?"} + COLL --> RDM + RDM -->|Yes| SWAP["Swap README.md
with README.{id}.md"] + RDM -->|No| VSCE + SWAP --> VSCE + + VSCE["vsce package --no-dependencies"] --> VSIX[".vsix output"] + + VSIX --> CLEAN["Finally: Cleanup"] + CLEAN --> R1["Restore package.json.bak"] + CLEAN --> R2["Restore README.md.bak"] + CLEAN --> R3["Remove .github/ docs/ scripts/"] + CLEAN --> R4["Restore original version"] +``` ### Manual Packaging (Legacy) @@ -344,13 +424,34 @@ The template file stays clean. Use `git checkout extension/package.json` to rest ### Collection Resolution -When building a collection, the system: +When building a collection, the system applies a multi-stage filter pipeline: persona matching, maturity gating, optional glob patterns, and two rounds of dependency resolution. + +```mermaid +flowchart TB + REG["Registry Entry
personas ยท maturity ยท requires"] --> PF{"Persona match?
empty personas = universal"} + CM["Collection Manifest
personas array"] --> PF + CH["Channel
Stable / PreRelease"] --> MF + + PF -->|Yes| MF{"Maturity
allowed?"} + PF -->|No| EXCLUDE[Excluded] + + MF -->|Yes| GLOB{"Passes include/exclude
glob filter?"} + MF -->|No| EXCLUDE + + GLOB -->|Yes| SEED[Seed Artifact] + GLOB -->|No| EXCLUDE + + SEED --> HANDOFF["Resolve Handoff Closure
BFS through agent frontmatter
handoff targets bypass maturity filter"] + HANDOFF --> REQUIRES["Resolve Requires Dependencies
BFS through registry requires blocks
across agents ยท prompts ยท instructions ยท skills"] + REQUIRES --> FINAL[Final Collection Artifact Set] +``` + +Key behaviors: -1. Reads the collection manifest to get the target personas -2. Reads the artifact registry (`.github/ai-artifacts-registry.json`) -3. Includes artifacts whose `personas` array contains any of the collection's personas -4. Includes artifacts with an empty `personas` array (universal artifacts shared across all collections) -5. Resolves artifact dependencies to ensure completeness +* Artifacts with an empty `personas` array are universal and included in every collection +* Handoff targets bypass maturity filtering by design (an agent must be able to hand off to its declared targets) +* The `requires` block supports transitive resolution: if agent A requires agent B, and B requires instruction C, all three are included +* Optional `include` and `exclude` glob arrays in the collection manifest provide fine-grained control per artifact type ### Testing Collection Builds Locally From 85d424f30d526f6528dad40a1a72085b2f2aab8c Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 10:39:49 +0100 Subject: [PATCH 26/62] feat(extension): add collection-level maturity gating for release channels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add maturity field to collection schema with stable/preview/experimental/deprecated enum - add Test-CollectionMaturityEligible to gate collections by channel in Prepare-Extension - add Test-CollectionManifests validation step to Validate-ArtifactRegistry - filter collections by maturity in extension-package.yml discover step - add 26 Pester tests covering maturity eligibility and collection validation Closes #467 ๐Ÿš€ - Generated by Copilot? --- .github/workflows/extension-package.yml | 23 +- docs/architecture/ai-artifacts.md | 32 ++- extension/PACKAGING.md | 35 ++- .../collections/developer.collection.json | 1 + .../collections/hve-core-all.collection.json | 1 + scripts/extension/Prepare-Extension.ps1 | 72 ++++++ scripts/linting/Validate-ArtifactRegistry.ps1 | 98 ++++++++ .../linting/schemas/collection.schema.json | 11 + .../extension/Prepare-Extension.Tests.ps1 | 163 +++++++++++++ .../Validate-ArtifactRegistry.Tests.ps1 | 218 ++++++++++++++++++ 10 files changed, 635 insertions(+), 19 deletions(-) diff --git a/.github/workflows/extension-package.yml b/.github/workflows/extension-package.yml index 5e1ae5ca..429fa5a1 100644 --- a/.github/workflows/extension-package.yml +++ b/.github/workflows/extension-package.yml @@ -50,16 +50,35 @@ jobs: shell: bash run: | collections_dir="extension/collections" + channel="${{ inputs.channel }}" + if [ -z "$channel" ]; then + channel="Stable" + fi + if [ ! -d "$collections_dir" ]; then echo "::error::Collections directory not found: $collections_dir" exit 1 fi - # Build JSON array of collection manifests + # Build JSON array of collection manifests, filtering by maturity matrix_json=$(find "$collections_dir" -name '*.collection.json' -type f | sort | while IFS= read -r file; do id=$(jq -r '.id' "$file") name=$(jq -r '.name' "$file") - echo "{\"id\":\"$id\",\"name\":\"$name\",\"manifest\":\"$file\"}" + maturity=$(jq -r '.maturity // "stable"' "$file") + + # Skip deprecated collections for all channels + if [ "$maturity" = "deprecated" ]; then + echo "::notice::Skipping deprecated collection: $id" >&2 + continue + fi + + # Skip experimental collections for Stable channel + if [ "$channel" = "Stable" ] && [ "$maturity" = "experimental" ]; then + echo "::notice::Skipping experimental collection '$id' for Stable channel" >&2 + continue + fi + + echo "{\"id\":\"$id\",\"name\":\"$name\",\"manifest\":\"$file\",\"maturity\":\"$maturity\"}" done | jq -s '{include: .}') echo "matrix=$matrix_json" >> "$GITHUB_OUTPUT" diff --git a/docs/architecture/ai-artifacts.md b/docs/architecture/ai-artifacts.md index 48e1171b..d89de4cc 100644 --- a/docs/architecture/ai-artifacts.md +++ b/docs/architecture/ai-artifacts.md @@ -21,16 +21,13 @@ Prompts (`.prompt.md`) serve as workflow entry points. They capture user intent * Define single-session workflows with clear inputs and outputs * Accept user inputs through `${input:varName}` template syntax * Delegate to agents via `agent:` frontmatter references -* Specify invocation context through `mode:` field values **Frontmatter structure:** ```yaml --- description: 'Protocol for creating ADO pull requests' -mode: 'workflow' agent: 'task-planner' -maturity: 'stable' --- ``` @@ -54,7 +51,6 @@ Agents (`.agent.md`) define task-specific behaviors with access to Copilot tools description: 'Orchestrates task planning with research integration' tools: ['codebase', 'search', 'editFiles', 'changes'] handoffs: ['task-implementor', 'task-researcher'] -maturity: 'stable' --- ``` @@ -77,7 +73,6 @@ Instructions (`.instructions.md`) encode technology-specific standards and conve --- description: 'Python scripting standards with type hints' applyTo: '**/*.py, **/*.ipynb' -maturity: 'stable' --- ``` @@ -277,7 +272,7 @@ Artifacts assigned to `hve-core-all` appear in the full collection and may also ### Collection Build System -Collections define persona-filtered artifact packages. Each collection manifest specifies which personas to include: +Collections define persona-filtered artifact packages. Each collection manifest specifies which personas to include and controls release channel eligibility through a `maturity` field: ```json { @@ -285,6 +280,7 @@ Collections define persona-filtered artifact packages. Each collection manifest "name": "hve-developer", "displayName": "HVE Core - Developer Edition", "description": "AI-powered coding agents curated for software engineers", + "maturity": "stable", "personas": ["developer"] } ``` @@ -292,10 +288,24 @@ Collections define persona-filtered artifact packages. Each collection manifest The build system resolves collections by: 1. Reading the collection manifest to identify target personas -2. Filtering registry entries by persona membership -3. Including the `hve-core-all` persona artifacts as the base -4. Adding persona-specific artifacts -5. Resolving dependencies for included artifacts +2. Checking collection-level maturity against the target release channel +3. Filtering registry entries by persona membership +4. Including the `hve-core-all` persona artifacts as the base +5. Adding persona-specific artifacts +6. Resolving dependencies for included artifacts + +#### Collection Maturity + +Collections carry their own maturity level, independent of artifact-level maturity. This controls whether the entire collection is built for a given release channel: + +| Collection Maturity | PreRelease Channel | Stable Channel | +| ------------------- | ------------------ | -------------- | +| `stable` | Included | Included | +| `preview` | Included | Included | +| `experimental` | Included | Excluded | +| `deprecated` | Excluded | Excluded | + +New collections should start as `experimental` until validated, then transition to `stable` by changing a single field. The `maturity` field is optional and defaults to `stable` when omitted. ### Dependency Resolution @@ -335,6 +345,8 @@ Artifact inclusion is controlled by the registry. Repo-specific instructions und | `experimental` | Excluded | Included | | `deprecated` | Excluded | Excluded | +The maturity table above applies to individual artifacts. Collections also carry a `maturity` field that gates the entire package at the channel level (see [Collection Maturity](#collection-maturity)). + ### Collection Packages Multiple extension packages can be built from the same codebase: diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index 4c066734..b60ea542 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -506,17 +506,34 @@ Collection manifests follow this structure: "name": "hve-developer", "displayName": "HVE Core - Developer Edition", "description": "AI-powered coding agents curated for software engineers", + "maturity": "stable", "personas": ["developer"] } ``` -| Field | Required | Description | -|---------------|----------|-----------------------------------------| -| `id` | Yes | Unique identifier for the collection | -| `name` | Yes | Extension package name | -| `displayName` | Yes | Marketplace display name | -| `description` | Yes | Marketplace description text | -| `personas` | Yes | Array of persona identifiers to include | +| Field | Required | Description | +|---------------|----------|-----------------------------------------------------------------------| +| `id` | Yes | Unique identifier for the collection | +| `name` | Yes | Extension package name | +| `displayName` | Yes | Marketplace display name | +| `description` | Yes | Marketplace description text | +| `maturity` | No | Release channel eligibility (`stable`, `preview`, `experimental`, `deprecated`). Defaults to `stable` | +| `personas` | Yes | Array of persona identifiers to include | + +#### Collection Maturity and Channel Eligibility + +The `maturity` field controls which release channels include the collection: + +| Collection Maturity | PreRelease Channel | Stable Channel | +| ------------------- | ------------------ | -------------- | +| `stable` | Yes | Yes | +| `preview` | Yes | Yes | +| `experimental` | Yes | No | +| `deprecated` | No | No | + +Collection-level maturity is independent of artifact-level maturity. A `stable` collection can contain `preview` artifacts, which are filtered by the existing artifact-level channel logic. The collection maturity gates the entire package, while artifact maturity gates individual files within it. + +Omitting the `maturity` field defaults to `stable`, maintaining backward compatibility with existing manifests. ### Adding New Collections @@ -531,6 +548,7 @@ To create a new persona collection: "name": "hve-my-persona", "displayName": "HVE Core - My Persona Edition", "description": "Description of artifacts included for this persona", + "maturity": "experimental", "personas": ["my-persona"] } ``` @@ -540,6 +558,9 @@ To create a new persona collection: 4. Test the build locally with `-Collection my-persona` 5. Submit PR with the new collection manifest +> [!TIP] +> New collections should start with `"maturity": "experimental"` until validated. Change to `"stable"` when the collection is ready for production. + ## Notes * The `.github`, `docs/templates`, and `scripts/dev-tools` folders are temporarily copied during packaging (not permanently stored) diff --git a/extension/collections/developer.collection.json b/extension/collections/developer.collection.json index 0e85c429..d9c14cb8 100644 --- a/extension/collections/developer.collection.json +++ b/extension/collections/developer.collection.json @@ -4,6 +4,7 @@ "name": "hve-developer", "displayName": "HVE Core - Developer Edition", "description": "AI-powered coding agents and prompts curated for software engineers", + "maturity": "experimental", "personas": [ "developer" ] diff --git a/extension/collections/hve-core-all.collection.json b/extension/collections/hve-core-all.collection.json index 7e3fb611..d2b9f4f2 100644 --- a/extension/collections/hve-core-all.collection.json +++ b/extension/collections/hve-core-all.collection.json @@ -4,6 +4,7 @@ "name": "hve-core", "displayName": "HVE Core", "description": "AI-powered chat agents, prompts, and instructions for hybrid virtual environments", + "maturity": "stable", "personas": [ "hve-core-all" ] diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index f1431bb0..d229a52b 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -91,6 +91,68 @@ function Get-AllowedMaturities { return @('stable') } +function Test-CollectionMaturityEligible { + <# + .SYNOPSIS + Checks whether a collection is eligible for the specified release channel. + .DESCRIPTION + Pure function that evaluates collection-level maturity against channel rules. + Experimental collections are eligible only for PreRelease. Deprecated collections + are excluded from all channels. + .PARAMETER CollectionManifest + Parsed collection manifest hashtable. + .PARAMETER Channel + Release channel ('Stable' or 'PreRelease'). + .OUTPUTS + [hashtable] With IsEligible bool and Reason string. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$CollectionManifest, + + [Parameter(Mandatory = $true)] + [ValidateSet('Stable', 'PreRelease')] + [string]$Channel + ) + + $maturity = 'stable' + if ($CollectionManifest.ContainsKey('maturity') -and $CollectionManifest['maturity']) { + $maturity = $CollectionManifest['maturity'] + } + + switch ($maturity) { + 'deprecated' { + return @{ + IsEligible = $false + Reason = "Collection '$($CollectionManifest.id)' is deprecated and excluded from all channels" + } + } + 'experimental' { + if ($Channel -eq 'Stable') { + return @{ + IsEligible = $false + Reason = "Collection '$($CollectionManifest.id)' is experimental and excluded from Stable channel" + } + } + return @{ IsEligible = $true; Reason = '' } + } + 'preview' { + return @{ IsEligible = $true; Reason = '' } + } + 'stable' { + return @{ IsEligible = $true; Reason = '' } + } + default { + return @{ + IsEligible = $false + Reason = "Collection '$($CollectionManifest.id)' has invalid maturity value: $maturity" + } + } + } +} + function Get-RegistryData { <# .SYNOPSIS @@ -1151,6 +1213,16 @@ function Invoke-PrepareExtension { $collectionManifest = Get-CollectionManifest -CollectionPath $Collection Write-Host "Collection: $($collectionManifest.displayName) ($($collectionManifest.id))" + # Check collection-level maturity eligibility + $collectionEligibility = Test-CollectionMaturityEligible -CollectionManifest $collectionManifest -Channel $Channel + if (-not $collectionEligibility.IsEligible) { + Write-Host "`nโญ๏ธ $($collectionEligibility.Reason)" -ForegroundColor Yellow + return New-PrepareResult -Success $true -Version $version + } + + $collectionMaturity = if ($collectionManifest.ContainsKey('maturity')) { $collectionManifest['maturity'] } else { 'stable' } + Write-Host "Collection maturity: $collectionMaturity" + # Get persona-filtered artifact names $collectionArtifactNames = Get-CollectionArtifacts -Registry $registry -Collection $collectionManifest -AllowedMaturities $allowedMaturities diff --git a/scripts/linting/Validate-ArtifactRegistry.ps1 b/scripts/linting/Validate-ArtifactRegistry.ps1 index de954b4e..b9dd8633 100644 --- a/scripts/linting/Validate-ArtifactRegistry.ps1 +++ b/scripts/linting/Validate-ArtifactRegistry.ps1 @@ -613,6 +613,99 @@ function Find-OrphanArtifacts { return @{ Warnings = $warnings } } +function Test-CollectionManifests { + <# + .SYNOPSIS + Validates collection manifest files against their schema and registry. + .DESCRIPTION + Checks all collection manifest files in extension/collections/ for: + - Valid JSON structure + - Valid maturity enum values + - Persona references matching registry definitions + - JSON Schema validation (PowerShell 7.4+) + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory)] + [hashtable]$Registry, + + [Parameter(Mandatory)] + [string]$RepoRoot + ) + + $errors = [System.Collections.Generic.List[string]]::new() + $warnings = [System.Collections.Generic.List[string]]::new() + + $collectionsDir = Join-Path $RepoRoot 'extension/collections' + if (-not (Test-Path $collectionsDir)) { + return @{ Success = $true; Errors = $errors; Warnings = $warnings } + } + + $validMaturity = @('stable', 'preview', 'experimental', 'deprecated') + $definedPersonas = @() + if ($Registry.ContainsKey('personas') -and $Registry['personas'].ContainsKey('definitions')) { + $definedPersonas = @($Registry['personas']['definitions'].Keys) + } + + $collectionFiles = Get-ChildItem -Path $collectionsDir -Filter '*.collection.json' -File -ErrorAction SilentlyContinue + + # JSON Schema validation for collection manifests + $collectionSchemaPath = Join-Path $RepoRoot 'scripts/linting/schemas/collection.schema.json' + + foreach ($file in $collectionFiles) { + $prefix = "collection/$($file.BaseName -replace '\.collection$', '')" + + try { + $content = Get-Content -Path $file.FullName -Raw + $manifest = $content | ConvertFrom-Json -AsHashtable + } + catch { + $errors.Add("${prefix}: Failed to parse JSON: $_") + continue + } + + # Validate maturity value if present + if ($manifest.ContainsKey('maturity')) { + if ($manifest['maturity'] -notin $validMaturity) { + $errors.Add("${prefix}: invalid maturity '$($manifest['maturity'])'. Must be one of: $($validMaturity -join ', ')") + } + } + + # Validate persona references + if ($manifest.ContainsKey('personas')) { + foreach ($persona in $manifest['personas']) { + if ($definedPersonas.Count -gt 0 -and $persona -notin $definedPersonas -and $persona -ne 'hve-core-all') { + $warnings.Add("${prefix}: references persona '$persona' not defined in registry") + } + } + } + + # Warn about deprecated collections that still exist in the build directory + if ($manifest.ContainsKey('maturity') -and $manifest['maturity'] -eq 'deprecated') { + $warnings.Add("${prefix}: collection is deprecated and will be excluded from all builds") + } + + # JSON Schema validation (PowerShell 7.4+) + if ((Test-Path $collectionSchemaPath) -and $PSVersionTable.PSVersion -ge [version]'7.4') { + try { + $schemaErrors = $null + $valid = Test-Json -Json $content -SchemaFile $collectionSchemaPath -ErrorAction SilentlyContinue -ErrorVariable schemaErrors + if (-not $valid) { + foreach ($schemaErr in $schemaErrors) { + $errors.Add("${prefix}: schema violation: $schemaErr") + } + } + } + catch { + $errors.Add("${prefix}: schema validation error: $($_.Exception.Message)") + } + } + } + + return @{ Success = ($errors.Count -eq 0); Errors = $errors; Warnings = $warnings } +} + #endregion Validation Functions #region Output Functions @@ -765,6 +858,11 @@ try { $orphanResult = Find-OrphanArtifacts -Registry $registry -RepoRoot $RepoRoot $allWarnings.AddRange($orphanResult.Warnings) + # Step 8: Collection manifest validation + $collectionResult = Test-CollectionManifests -Registry $registry -RepoRoot $RepoRoot + $allErrors.AddRange($collectionResult.Errors) + $allWarnings.AddRange($collectionResult.Warnings) + # Build result $result = @{ Success = ($allErrors.Count -eq 0) diff --git a/scripts/linting/schemas/collection.schema.json b/scripts/linting/schemas/collection.schema.json index 2fa9930b..5d41b792 100644 --- a/scripts/linting/schemas/collection.schema.json +++ b/scripts/linting/schemas/collection.schema.json @@ -33,6 +33,17 @@ "type": "string", "minLength": 1 }, + "maturity": { + "type": "string", + "enum": [ + "stable", + "preview", + "experimental", + "deprecated" + ], + "default": "stable", + "description": "Collection-level maturity controlling release channel eligibility. Experimental collections are released only as PreRelease. Deprecated collections are excluded from all releases." + }, "personas": { "type": "array", "items": { diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index 0140917d..75887be4 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -21,6 +21,86 @@ Describe 'Get-AllowedMaturities' { } +Describe 'Test-CollectionMaturityEligible' { + It 'Returns eligible for stable collection on Stable channel' { + $manifest = @{ id = 'test'; maturity = 'stable' } + $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable' + $result.IsEligible | Should -BeTrue + $result.Reason | Should -BeNullOrEmpty + } + + It 'Returns eligible for stable collection on PreRelease channel' { + $manifest = @{ id = 'test'; maturity = 'stable' } + $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease' + $result.IsEligible | Should -BeTrue + } + + It 'Returns eligible for preview collection on Stable channel' { + $manifest = @{ id = 'test'; maturity = 'preview' } + $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable' + $result.IsEligible | Should -BeTrue + } + + It 'Returns eligible for preview collection on PreRelease channel' { + $manifest = @{ id = 'test'; maturity = 'preview' } + $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease' + $result.IsEligible | Should -BeTrue + } + + It 'Returns ineligible for experimental collection on Stable channel' { + $manifest = @{ id = 'exp-coll'; maturity = 'experimental' } + $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable' + $result.IsEligible | Should -BeFalse + $result.Reason | Should -Match 'experimental.*excluded from Stable' + } + + It 'Returns eligible for experimental collection on PreRelease channel' { + $manifest = @{ id = 'exp-coll'; maturity = 'experimental' } + $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease' + $result.IsEligible | Should -BeTrue + } + + It 'Returns ineligible for deprecated collection on Stable channel' { + $manifest = @{ id = 'old-coll'; maturity = 'deprecated' } + $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable' + $result.IsEligible | Should -BeFalse + $result.Reason | Should -Match 'deprecated.*excluded from all channels' + } + + It 'Returns ineligible for deprecated collection on PreRelease channel' { + $manifest = @{ id = 'old-coll'; maturity = 'deprecated' } + $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease' + $result.IsEligible | Should -BeFalse + $result.Reason | Should -Match 'deprecated.*excluded from all channels' + } + + It 'Defaults to stable when maturity key is absent' { + $manifest = @{ id = 'no-maturity' } + $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable' + $result.IsEligible | Should -BeTrue + } + + It 'Defaults to stable when maturity value is empty string' { + $manifest = @{ id = 'empty-maturity'; maturity = '' } + $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable' + $result.IsEligible | Should -BeTrue + } + + It 'Returns ineligible for unknown maturity value' { + $manifest = @{ id = 'bad-coll'; maturity = 'alpha' } + $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'PreRelease' + $result.IsEligible | Should -BeFalse + $result.Reason | Should -Match 'invalid maturity value' + } + + It 'Returns hashtable with expected keys' { + $manifest = @{ id = 'test'; maturity = 'stable' } + $result = Test-CollectionMaturityEligible -CollectionManifest $manifest -Channel 'Stable' + $result.Keys | Should -Contain 'IsEligible' + $result.Keys | Should -Contain 'Reason' + } +} + Describe 'Test-PathsExist' { BeforeAll { $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) @@ -1080,4 +1160,87 @@ applyTo: "**/*.js" $bakContent | Should -Be $script:originalPackageJson } } + + Context 'Collection maturity gating' { + BeforeAll { + # Deprecated collection manifest + $script:deprecatedCollectionPath = Join-Path $script:tempDir 'deprecated.collection.json' + @{ + id = 'deprecated-coll' + name = 'deprecated-ext' + displayName = 'Deprecated Collection' + description = 'Deprecated collection for testing' + personas = @('hve-core-all') + maturity = 'deprecated' + } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:deprecatedCollectionPath + + # Experimental collection manifest + $script:experimentalCollectionPath = Join-Path $script:tempDir 'experimental.collection.json' + @{ + id = 'experimental-coll' + name = 'experimental-ext' + displayName = 'Experimental Collection' + description = 'Experimental collection for testing' + personas = @('hve-core-all') + maturity = 'experimental' + } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:experimentalCollectionPath + + # Persona template for experimental collection + @' +{ + "name": "experimental-ext", + "version": "1.2.3", + "contributes": {} +} +'@ | Set-Content -Path (Join-Path $script:extDir 'package.experimental-coll.json') + } + + It 'Returns early success for deprecated collection on Stable channel' { + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' ` + -Collection $script:deprecatedCollectionPath ` + -DryRun + + $result.Success | Should -BeTrue + $result.AgentCount | Should -Be 0 + } + + It 'Returns early success for deprecated collection on PreRelease channel' { + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'PreRelease' ` + -Collection $script:deprecatedCollectionPath ` + -DryRun + + $result.Success | Should -BeTrue + $result.AgentCount | Should -Be 0 + } + + It 'Returns early success for experimental collection on Stable channel' { + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' ` + -Collection $script:experimentalCollectionPath ` + -DryRun + + $result.Success | Should -BeTrue + $result.AgentCount | Should -Be 0 + } + + It 'Processes experimental collection on PreRelease channel' { + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'PreRelease' ` + -Collection $script:experimentalCollectionPath ` + -DryRun + + $result.Success | Should -BeTrue + $result.AgentCount | Should -BeGreaterThan 0 + } + } } diff --git a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 index caf02cf7..78bdd6f9 100644 --- a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 +++ b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 @@ -822,6 +822,224 @@ Describe 'Find-OrphanArtifacts' -Tag 'Unit' { #endregion +#region Test-CollectionManifests Tests + +Describe 'Test-CollectionManifests' -Tag 'Unit' { + BeforeAll { + $script:TestRoot = Join-Path $TestDrive 'collection-manifests-test' + $script:CollectionsDir = Join-Path $script:TestRoot 'extension/collections' + $script:SchemaDir = Join-Path $script:TestRoot 'scripts/linting/schemas' + + $script:BaseRegistry = @{ + personas = @{ + definitions = @{ + 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } + 'developer' = @{ name = 'Developer'; description = 'Developer artifacts' } + } + } + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + } + + BeforeEach { + if (Test-Path $script:TestRoot) { + Remove-Item -Path $script:TestRoot -Recurse -Force + } + New-Item -ItemType Directory -Path $script:CollectionsDir -Force | Out-Null + New-Item -ItemType Directory -Path $script:SchemaDir -Force | Out-Null + + # Copy real schema for JSON Schema validation + $realSchemaPath = Join-Path $PSScriptRoot '../../linting/schemas/collection.schema.json' + if (Test-Path $realSchemaPath) { + Copy-Item -Path $realSchemaPath -Destination (Join-Path $script:SchemaDir 'collection.schema.json') + } + } + + Context 'Missing collections directory' { + It 'Returns success when collections directory does not exist' { + $emptyRoot = Join-Path $TestDrive 'no-collections' + New-Item -ItemType Directory -Path $emptyRoot -Force | Out-Null + + $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $emptyRoot + $result.Success | Should -BeTrue + $result.Errors.Count | Should -Be 0 + $result.Warnings.Count | Should -Be 0 + } + } + + Context 'Valid manifests' { + It 'Passes for a valid collection manifest with stable maturity' { + @{ + '$schema' = '../schemas/collection.schema.json' + id = 'test-coll' + name = 'test-ext' + displayName = 'Test Collection' + description = 'A valid test collection' + personas = @('hve-core-all') + maturity = 'stable' + } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'test-coll.collection.json') + + $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot + $result.Success | Should -BeTrue + $result.Errors.Count | Should -Be 0 + } + + It 'Passes for manifest without maturity field (defaults assumed)' { + @{ + '$schema' = '../schemas/collection.schema.json' + id = 'no-maturity' + name = 'no-maturity-ext' + displayName = 'No Maturity' + description = 'Collection without maturity field' + personas = @('developer') + } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'no-maturity.collection.json') + + $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot + $result.Success | Should -BeTrue + } + } + + Context 'Invalid maturity values' { + It 'Reports error for invalid maturity enum value' { + @{ + '$schema' = '../schemas/collection.schema.json' + id = 'bad-maturity' + name = 'bad-maturity-ext' + displayName = 'Bad Maturity' + description = 'Collection with invalid maturity' + personas = @('hve-core-all') + maturity = 'alpha' + } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'bad-maturity.collection.json') + + $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot + $result.Success | Should -BeFalse + $result.Errors | Where-Object { $_ -match "invalid maturity 'alpha'" } | Should -Not -BeNullOrEmpty + } + } + + Context 'Deprecated collection warnings' { + It 'Emits warning for deprecated collection' { + @{ + '$schema' = '../schemas/collection.schema.json' + id = 'old-coll' + name = 'old-ext' + displayName = 'Old Collection' + description = 'A deprecated collection' + personas = @('hve-core-all') + maturity = 'deprecated' + } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'old-coll.collection.json') + + $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot + $result.Success | Should -BeTrue + $result.Warnings | Where-Object { $_ -match 'deprecated.*excluded from all builds' } | Should -Not -BeNullOrEmpty + } + } + + Context 'Persona reference validation' { + It 'Warns when persona is not defined in registry' { + @{ + '$schema' = '../schemas/collection.schema.json' + id = 'unknown-persona' + name = 'unknown-persona-ext' + displayName = 'Unknown Persona' + description = 'Collection with undefined persona' + personas = @('nonexistent-persona') + maturity = 'stable' + } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'unknown-persona.collection.json') + + $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot + $result.Warnings | Where-Object { $_ -match "references persona 'nonexistent-persona' not defined" } | Should -Not -BeNullOrEmpty + } + + It 'Does not warn for hve-core-all persona even when not in definitions' { + $emptyPersonaRegistry = @{ + personas = @{ definitions = @{} } + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + @{ + '$schema' = '../schemas/collection.schema.json' + id = 'all-coll' + name = 'all-ext' + displayName = 'All Collection' + description = 'Uses hve-core-all persona' + personas = @('hve-core-all') + maturity = 'stable' + } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'all-coll.collection.json') + + $result = Test-CollectionManifests -Registry $emptyPersonaRegistry -RepoRoot $script:TestRoot + $result.Warnings | Where-Object { $_ -match 'hve-core-all' } | Should -BeNullOrEmpty + } + } + + Context 'Invalid JSON' { + It 'Reports error for malformed JSON file' { + '{ invalid json }' | Set-Content -Path (Join-Path $script:CollectionsDir 'broken.collection.json') + + $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot + $result.Success | Should -BeFalse + $result.Errors | Where-Object { $_ -match 'Failed to parse JSON' } | Should -Not -BeNullOrEmpty + } + } + + Context 'Multiple manifest validation' { + It 'Validates all collection files in directory' { + # Valid manifest + @{ + '$schema' = '../schemas/collection.schema.json' + id = 'valid' + name = 'valid-ext' + displayName = 'Valid' + description = 'Valid collection' + personas = @('hve-core-all') + maturity = 'stable' + } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'valid.collection.json') + + # Invalid maturity manifest + @{ + '$schema' = '../schemas/collection.schema.json' + id = 'invalid' + name = 'invalid-ext' + displayName = 'Invalid' + description = 'Invalid collection' + personas = @('hve-core-all') + maturity = 'bogus' + } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'invalid.collection.json') + + $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot + $result.Success | Should -BeFalse + $result.Errors | Where-Object { $_ -match "invalid maturity 'bogus'" } | Should -Not -BeNullOrEmpty + } + } + + Context 'All valid maturity values accepted' { + It 'Accepts stable, preview, experimental, and deprecated maturities' { + foreach ($maturity in @('stable', 'preview', 'experimental', 'deprecated')) { + @{ + '$schema' = '../schemas/collection.schema.json' + id = "$maturity-coll" + name = "$maturity-ext" + displayName = "$maturity Collection" + description = "Collection with $maturity maturity" + personas = @('hve-core-all') + maturity = $maturity + } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir "$maturity.collection.json") + } + + $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot + # No maturity-related errors (deprecated warnings are fine) + $result.Errors | Where-Object { $_ -match 'invalid maturity' } | Should -BeNullOrEmpty + } + } +} + +#endregion + #region Write-RegistryValidationOutput Tests Describe 'Write-RegistryValidationOutput' -Tag 'Unit' { From 81cc56691dab02e04829f257f9c4ae93195eafba Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 10:42:50 +0100 Subject: [PATCH 27/62] docs(architecture): update AI Artifacts Architecture with new date and maturity tracking details --- docs/architecture/ai-artifacts.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/architecture/ai-artifacts.md b/docs/architecture/ai-artifacts.md index d89de4cc..4741a568 100644 --- a/docs/architecture/ai-artifacts.md +++ b/docs/architecture/ai-artifacts.md @@ -2,7 +2,7 @@ title: AI Artifacts Architecture description: Prompt, agent, and instruction delegation model for Copilot customizations author: Microsoft -ms.date: 2026-01-22 +ms.date: 2026-02-10 ms.topic: concept --- @@ -94,7 +94,6 @@ Skills (`.github/skills//SKILL.md`) provide executable utilities that agen * Provide self-contained utility packages with documentation and scripts * Include parallel implementations for cross-platform support (`.sh` and `.ps1`) * Execute actual operations rather than providing guidance -* Declare maturity level controlling extension channel inclusion **Directory structure:** @@ -114,7 +113,6 @@ Skills (`.github/skills//SKILL.md`) provide executable utilities that agen --- name: video-to-gif description: 'Video-to-GIF conversion with FFmpeg optimization' -maturity: stable --- ``` @@ -124,7 +122,8 @@ maturity: stable |---------------|---------------------------------------------------------| | `name` | Lowercase kebab-case identifier matching directory name | | `description` | Brief capability description | -| `maturity` | `stable`, `preview`, `experimental`, or `deprecated` | + +Maturity is tracked in the artifact registry, not in skill frontmatter. See [Artifact Registry](#artifact-registry) for details. Skills answer the question "what specialized utility does this task require?" and provide executable capabilities beyond conversational guidance. @@ -263,10 +262,10 @@ Each artifact entry contains metadata for filtering and dependency resolution: Personas represent user roles that consume artifacts. The registry defines these personas: -| Persona | Identifier | Target Users | -|---------------|----------------|---------------------| -| **All** | `hve-core-all` | Universal inclusion | -| **Developer** | `developer` | Software engineers | +| Persona | Identifier | Target Users | +|----------------------|--------------------|------------------------------------| +| **All** | `hve-core-all` | Universal inclusion | +| **Developer** | `developer` | Software engineers | Artifacts assigned to `hve-core-all` appear in the full collection and may also include role-specific personas for targeted distribution. From 7134e7e27ec04c12e6ae666296a9c2fa27222286 Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 10:49:08 +0100 Subject: [PATCH 28/62] feat(workflows): add PR validation for AI artifact registry drift detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add reusable artifact-registry-validation.yml workflow with soft-fail and warnings-as-errors inputs - wire registry validation job into pr-validation.yml - add renamed-file-mismatch test fixture for dual-direction detection - add 5 Pester tests covering add, remove, rename, subdirectory, and hve-core exclusion scenarios Closes #468 ๐Ÿ” - Generated by Copilot --- .../artifact-registry-validation.yml | 65 +++++++++++ .github/workflows/pr-validation.yml | 9 ++ .../renamed-file-mismatch.json | 26 +++++ .../Validate-ArtifactRegistry.Tests.ps1 | 110 ++++++++++++++++++ 4 files changed, 210 insertions(+) create mode 100644 .github/workflows/artifact-registry-validation.yml create mode 100644 scripts/tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json diff --git a/.github/workflows/artifact-registry-validation.yml b/.github/workflows/artifact-registry-validation.yml new file mode 100644 index 00000000..2f6b7b9f --- /dev/null +++ b/.github/workflows/artifact-registry-validation.yml @@ -0,0 +1,65 @@ +name: Artifact Registry Validation + +on: + workflow_call: + inputs: + soft-fail: + description: 'Whether to continue on registry validation errors' + required: false + type: boolean + default: false + warnings-as-errors: + description: 'Treat warnings as errors' + required: false + type: boolean + default: true + +permissions: + contents: read + +jobs: + artifact-registry-validation: + name: Validate Artifact Registry + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 + with: + persist-credentials: false + + - name: Create logs directory + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path logs | Out-Null + + - name: Run Artifact Registry Validation + id: validate + shell: pwsh + run: | + $params = @{} + + if ('${{ inputs.warnings-as-errors }}' -eq 'true') { + $params['WarningsAsErrors'] = $true + } + + & scripts/linting/Validate-ArtifactRegistry.ps1 @params + continue-on-error: true + + - name: Upload registry validation results + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4.4.3 + with: + name: registry-validation-results + path: logs/registry-validation-results.json + retention-days: 30 + + - name: Check results and fail if needed + if: ${{ !inputs.soft-fail }} + shell: pwsh + run: | + if ('${{ steps.validate.outcome }}' -eq 'failure') { + Write-Host "Artifact registry validation failed and soft-fail is false. Failing the job." + exit 1 + } diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index c81a667d..510c9513 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -89,6 +89,15 @@ jobs: skip-footer-validation: false warnings-as-errors: true + artifact-registry-validation: + name: Artifact Registry Validation + uses: ./.github/workflows/artifact-registry-validation.yml + permissions: + contents: read + with: + soft-fail: false + warnings-as-errors: true + link-lang-check: name: Link Language Check uses: ./.github/workflows/link-lang-check.yml diff --git a/scripts/tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json b/scripts/tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json new file mode 100644 index 00000000..1ba05dbd --- /dev/null +++ b/scripts/tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json @@ -0,0 +1,26 @@ +{ + "$schema": "../schemas/ai-artifacts-registry.schema.json", + "version": "1.0", + "personas": { + "definitions": { + "developer": { + "name": "Developer", + "description": "Software engineers building applications" + } + } + }, + "agents": { + "old-agent-name": { + "maturity": "stable", + "personas": [ + "developer" + ], + "tags": [ + "test" + ] + } + }, + "prompts": {}, + "instructions": {}, + "skills": {} +} \ No newline at end of file diff --git a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 index 78bdd6f9..23f07a01 100644 --- a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 +++ b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 @@ -32,6 +32,7 @@ BeforeAll { $script:ExtraPropertiesPath = Join-Path $script:FixtureDir 'extra-properties.json' $script:InvalidPersonaFormatInArtifactPath = Join-Path $script:FixtureDir 'invalid-persona-format-in-artifact.json' $script:ExtraPersonaPropertiesPath = Join-Path $script:FixtureDir 'extra-persona-properties.json' + $script:RenamedFileMismatchPath = Join-Path $script:FixtureDir 'renamed-file-mismatch.json' $script:SchemaPath = Join-Path $PSScriptRoot '../../linting/schemas/ai-artifacts-registry.schema.json' } @@ -822,6 +823,115 @@ Describe 'Find-OrphanArtifacts' -Tag 'Unit' { #endregion +#region Renamed-File Mismatch Tests (Dual-Direction Detection) + +Describe 'Renamed-File Mismatch Detection' -Tag 'Unit' { + BeforeAll { + $script:RenameTestRoot = Join-Path $TestDrive 'rename-test-repo' + } + + BeforeEach { + if (Test-Path $script:RenameTestRoot) { + Remove-Item -Path $script:RenameTestRoot -Recurse -Force + } + New-Item -ItemType Directory -Path "$script:RenameTestRoot/.github/agents" -Force | Out-Null + New-Item -ItemType Directory -Path "$script:RenameTestRoot/.github/prompts" -Force | Out-Null + New-Item -ItemType Directory -Path "$script:RenameTestRoot/.github/instructions" -Force | Out-Null + New-Item -ItemType Directory -Path "$script:RenameTestRoot/.github/skills" -Force | Out-Null + } + + Context 'File added but not in registry (orphan promoted to error with -WarningsAsErrors)' { + It 'Detects orphan file and treats as error when WarningsAsErrors is enabled' { + Set-Content -Path "$script:RenameTestRoot/.github/agents/new-unregistered.agent.md" -Value '# New Agent' + $registry = @{ + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + $orphanResult = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:RenameTestRoot + $orphanResult.Warnings | Where-Object { $_ -match 'new-unregistered' } | Should -Not -BeNullOrEmpty + } + } + + Context 'File removed but still in registry (existence error)' { + It 'Detects missing file for stale registry key' { + # Registry references old-agent-name but no file exists + $content = Get-Content $script:RenamedFileMismatchPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + $result = Test-ArtifactFileExistence -Registry $registry -RepoRoot $script:RenameTestRoot + $result.Success | Should -BeFalse + $result.Errors | Where-Object { $_ -match 'agents/old-agent-name: File not found' } | Should -Not -BeNullOrEmpty + } + } + + Context 'Renamed file: old key errors AND new file detected as orphan' { + It 'Detects both old-key existence error and new-file orphan warning' { + # Registry has old-agent-name; file was renamed to new-agent-name on disk + Set-Content -Path "$script:RenameTestRoot/.github/agents/new-agent-name.agent.md" -Value '# Renamed Agent' + + $content = Get-Content $script:RenamedFileMismatchPath -Raw + $registry = $content | ConvertFrom-Json -AsHashtable + + # Old key should fail existence check + $existenceResult = Test-ArtifactFileExistence -Registry $registry -RepoRoot $script:RenameTestRoot + $existenceResult.Success | Should -BeFalse + $existenceResult.Errors | Where-Object { $_ -match 'agents/old-agent-name: File not found' } | Should -Not -BeNullOrEmpty + + # New file should be detected as orphan + $orphanResult = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:RenameTestRoot + $orphanResult.Warnings | Where-Object { $_ -match 'new-agent-name' } | Should -Not -BeNullOrEmpty + } + + It 'Covers instruction file rename across subdirectories' { + # Registry has instruction key "subdir/old-instruction"; file renamed to "subdir/new-instruction" + New-Item -ItemType Directory -Path "$script:RenameTestRoot/.github/instructions/subdir" -Force | Out-Null + Set-Content -Path "$script:RenameTestRoot/.github/instructions/subdir/new-instruction.instructions.md" -Value '# Renamed Instruction' + + $registry = @{ + agents = @{} + prompts = @{} + instructions = @{ + 'subdir/old-instruction' = @{ + maturity = 'stable' + personas = @('developer') + tags = @('test') + } + } + skills = @{} + } + + # Old key should fail existence check + $existenceResult = Test-ArtifactFileExistence -Registry $registry -RepoRoot $script:RenameTestRoot + $existenceResult.Success | Should -BeFalse + $existenceResult.Errors | Where-Object { $_ -match 'instructions/subdir/old-instruction: File not found' } | Should -Not -BeNullOrEmpty + + # New file should be detected as orphan + $orphanResult = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:RenameTestRoot + $orphanResult.Warnings | Where-Object { $_ -match 'new-instruction' } | Should -Not -BeNullOrEmpty + } + } + + Context 'hve-core exclusion preserved during rename scenarios' { + It 'Does not report orphan for renamed file under hve-core directory' { + New-Item -ItemType Directory -Path "$script:RenameTestRoot/.github/instructions/hve-core" -Force | Out-Null + Set-Content -Path "$script:RenameTestRoot/.github/instructions/hve-core/renamed-workflow.instructions.md" -Value '# Renamed' + + $registry = @{ + agents = @{} + prompts = @{} + instructions = @{} + skills = @{} + } + + $orphanResult = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:RenameTestRoot + $orphanResult.Warnings | Where-Object { $_ -match 'hve-core' } | Should -BeNullOrEmpty + } + } +} + +#endregion + #region Test-CollectionManifests Tests Describe 'Test-CollectionManifests' -Tag 'Unit' { From b9d20a7dca4792a632068b9bb344d17e4e3e6964 Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 10:59:31 +0100 Subject: [PATCH 29/62] fix(tests): update schema paths in artifact registry JSON fixtures --- scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json | 2 +- .../Fixtures/ArtifactRegistry/extra-persona-properties.json | 2 +- scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json | 2 +- scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json | 2 +- scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json | 2 +- .../ArtifactRegistry/invalid-persona-format-in-artifact.json | 2 +- scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json | 2 +- scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json | 2 +- .../Fixtures/ArtifactRegistry/missing-artifact-fields.json | 2 +- .../tests/Fixtures/ArtifactRegistry/missing-persona-name.json | 2 +- .../tests/Fixtures/ArtifactRegistry/missing-personas-defs.json | 2 +- scripts/tests/Fixtures/ArtifactRegistry/no-requires.json | 2 +- .../tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json | 2 +- .../tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json | 2 +- scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json | 2 +- scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json b/scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json index 53a48f5d..805d7144 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/extra-persona-properties.json b/scripts/tests/Fixtures/ArtifactRegistry/extra-persona-properties.json index 12e1ea8f..dd5f7872 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/extra-persona-properties.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/extra-persona-properties.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json b/scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json index 6d7e960f..0b0de56f 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json index c3b43c22..a1980f1d 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json @@ -1,4 +1,4 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0""malformed JSON - missing comma after version" } \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json index 6af34425..cf62db4a 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-format-in-artifact.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-format-in-artifact.json index d93a68a8..0173d301 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-format-in-artifact.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-format-in-artifact.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json index bdc78993..99e7335d 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json index a5050e66..e3023d5e 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/missing-artifact-fields.json b/scripts/tests/Fixtures/ArtifactRegistry/missing-artifact-fields.json index 13719260..716263ab 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/missing-artifact-fields.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/missing-artifact-fields.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/missing-persona-name.json b/scripts/tests/Fixtures/ArtifactRegistry/missing-persona-name.json index abdeeb8a..3998c0e9 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/missing-persona-name.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/missing-persona-name.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/missing-personas-defs.json b/scripts/tests/Fixtures/ArtifactRegistry/missing-personas-defs.json index a744e750..864a985f 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/missing-personas-defs.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/missing-personas-defs.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "note": "Missing definitions key" diff --git a/scripts/tests/Fixtures/ArtifactRegistry/no-requires.json b/scripts/tests/Fixtures/ArtifactRegistry/no-requires.json index 9d123960..9d692ee9 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/no-requires.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/no-requires.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json b/scripts/tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json index 1ba05dbd..add174f4 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json b/scripts/tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json index 80ea9961..e2656831 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json b/scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json index 3231b117..a7d16518 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { diff --git a/scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json b/scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json index 5fcb0b26..39ecb45c 100644 --- a/scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json +++ b/scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json @@ -1,5 +1,5 @@ { - "$schema": "../schemas/ai-artifacts-registry.schema.json", + "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", "version": "1.0", "personas": { "definitions": { From 8d2120279f730c893d8597208c8aa16adf726399 Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 11:18:15 +0100 Subject: [PATCH 30/62] feat(extension): add tests for persona README path retrieval and management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - implement tests for Get-PersonaReadmePath function - add tests for Set-PersonaReadme function - include tests for Copy-CollectionArtifacts function - validate Invoke-PackageExtension behavior with collections ๐Ÿ” - Generated by Copilot --- .../extension/Package-Extension.Tests.ps1 | 303 ++++++++++++++++++ 1 file changed, 303 insertions(+) diff --git a/scripts/tests/extension/Package-Extension.Tests.ps1 b/scripts/tests/extension/Package-Extension.Tests.ps1 index eb8831f3..7709d623 100644 --- a/scripts/tests/extension/Package-Extension.Tests.ps1 +++ b/scripts/tests/extension/Package-Extension.Tests.ps1 @@ -912,6 +912,309 @@ Describe 'Restore-PackageJsonVersion' { } } +Describe 'Get-PersonaReadmePath' { + BeforeAll { + $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "persona-readme-test-$([guid]::NewGuid().ToString('N').Substring(0,8))" + $script:extDir = Join-Path $script:testDir 'extension' + } + + BeforeEach { + New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null + } + + AfterEach { + if (Test-Path $script:testDir) { + Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It 'Returns null for hve-core-all collection' { + $collectionPath = Join-Path $script:testDir 'collection.json' + @{ id = 'hve-core-all'; name = 'all' } | ConvertTo-Json | Set-Content $collectionPath + + $result = Get-PersonaReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir + $result | Should -BeNullOrEmpty + } + + It 'Returns persona README path when file exists' { + $collectionPath = Join-Path $script:testDir 'collection.json' + @{ id = 'developer'; name = 'dev' } | ConvertTo-Json | Set-Content $collectionPath + + $personaReadme = Join-Path $script:extDir 'README.developer.md' + Set-Content -Path $personaReadme -Value '# Developer README' + + $result = Get-PersonaReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir + $result | Should -Be $personaReadme + } + + It 'Returns null when persona README file does not exist' { + $collectionPath = Join-Path $script:testDir 'collection.json' + @{ id = 'security'; name = 'sec' } | ConvertTo-Json | Set-Content $collectionPath + + $result = Get-PersonaReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir + $result | Should -BeNullOrEmpty + } +} + +Describe 'Set-PersonaReadme' { + BeforeAll { + $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "set-readme-test-$([guid]::NewGuid().ToString('N').Substring(0,8))" + } + + BeforeEach { + New-Item -Path $script:testDir -ItemType Directory -Force | Out-Null + Set-Content -Path (Join-Path $script:testDir 'README.md') -Value '# Original README' + } + + AfterEach { + if (Test-Path $script:testDir) { + Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It 'Swaps README.md with persona README and creates backup' { + $personaPath = Join-Path $script:testDir 'README.developer.md' + Set-Content -Path $personaPath -Value '# Developer README' + + Set-PersonaReadme -ExtensionDirectory $script:testDir -PersonaReadmePath $personaPath -Operation Swap + + $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw + $readmeContent | Should -Match 'Developer README' + + Test-Path (Join-Path $script:testDir 'README.md.bak') | Should -BeTrue + $backupContent = Get-Content -Path (Join-Path $script:testDir 'README.md.bak') -Raw + $backupContent | Should -Match 'Original README' + } + + It 'Warns and returns early when no persona path for swap' { + Mock Write-Warning {} + Set-PersonaReadme -ExtensionDirectory $script:testDir -Operation Swap + + Should -Invoke Write-Warning -Times 1 + $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw + $readmeContent | Should -Match 'Original README' + } + + It 'Restores README.md from backup and removes backup file' { + # Create backup state + Set-Content -Path (Join-Path $script:testDir 'README.md.bak') -Value '# Original README' + Set-Content -Path (Join-Path $script:testDir 'README.md') -Value '# Persona README' + + Set-PersonaReadme -ExtensionDirectory $script:testDir -Operation Restore + + $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw + $readmeContent | Should -Match 'Original README' + Test-Path (Join-Path $script:testDir 'README.md.bak') | Should -BeFalse + } + + It 'Restore is a no-op when no backup exists' { + { Set-PersonaReadme -ExtensionDirectory $script:testDir -Operation Restore } | Should -Not -Throw + $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw + $readmeContent | Should -Match 'Original README' + } +} + +Describe 'Copy-CollectionArtifacts' { + BeforeAll { + $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "copy-col-test-$([guid]::NewGuid().ToString('N').Substring(0,8))" + $script:extDir = Join-Path $script:testDir 'extension' + $script:repoRoot = Join-Path $script:testDir 'repo' + } + + BeforeEach { + New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null + New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null + } + + AfterEach { + if (Test-Path $script:testDir) { + Remove-Item -Path $script:testDir -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It 'Copies agents from repo to extension directory' { + # Create source agent + $agentsSrc = Join-Path $script:repoRoot '.github/agents' + New-Item -Path $agentsSrc -ItemType Directory -Force | Out-Null + Set-Content -Path (Join-Path $agentsSrc 'task-planner.agent.md') -Value '# Agent' + + # Create package.json with contributes referencing agents + $pkgJson = @{ + contributes = @{ + chatAgents = @( + @{ path = './.github/agents/task-planner.agent.md' } + ) + } + } + $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json') + + Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{} + + Test-Path (Join-Path $script:extDir '.github/agents/task-planner.agent.md') | Should -BeTrue + } + + It 'Copies prompts from repo to extension directory' { + # Create source prompt + $promptsSrc = Join-Path $script:repoRoot '.github/prompts' + New-Item -Path $promptsSrc -ItemType Directory -Force | Out-Null + Set-Content -Path (Join-Path $promptsSrc 'my-prompt.prompt.md') -Value '# Prompt' + + $pkgJson = @{ + contributes = @{ + chatPromptFiles = @( + @{ path = './.github/prompts/my-prompt.prompt.md' } + ) + } + } + $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json') + + Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{} + + Test-Path (Join-Path $script:extDir '.github/prompts/my-prompt.prompt.md') | Should -BeTrue + } + + It 'Copies instructions from repo to extension directory' { + # Create source instruction + $instrSrc = Join-Path $script:repoRoot '.github/instructions' + New-Item -Path $instrSrc -ItemType Directory -Force | Out-Null + Set-Content -Path (Join-Path $instrSrc 'commit-message.instructions.md') -Value '# Instructions' + + $pkgJson = @{ + contributes = @{ + chatInstructions = @( + @{ path = './.github/instructions/commit-message.instructions.md' } + ) + } + } + $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json') + + Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{} + + Test-Path (Join-Path $script:extDir '.github/instructions/commit-message.instructions.md') | Should -BeTrue + } + + It 'Copies skills recursively from repo to extension directory' { + # Create source skill with nested file + $skillSrc = Join-Path $script:repoRoot '.github/skills/video-to-gif' + New-Item -Path $skillSrc -ItemType Directory -Force | Out-Null + Set-Content -Path (Join-Path $skillSrc 'SKILL.md') -Value '# Skill' + + $pkgJson = @{ + contributes = @{ + chatSkills = @( + @{ path = './.github/skills/video-to-gif' } + ) + } + } + $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json') + + Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{} + + Test-Path (Join-Path $script:extDir '.github/skills/video-to-gif') | Should -BeTrue + } + + It 'Skips missing source files without error' { + $pkgJson = @{ + contributes = @{ + chatAgents = @( @{ path = './.github/agents/nonexistent.agent.md' } ) + chatPromptFiles = @( @{ path = './.github/prompts/nonexistent.prompt.md' } ) + chatInstructions = @( @{ path = './.github/instructions/nonexistent.instructions.md' } ) + chatSkills = @( @{ path = './.github/skills/nonexistent' } ) + } + } + $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json') + + { Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{} } | Should -Not -Throw + } + + It 'Handles empty contributes sections' { + $pkgJson = @{ contributes = @{} } + $pkgJson | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json') + + { Copy-CollectionArtifacts -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir -PrepareResult @{} } | Should -Not -Throw + } +} + +Describe 'Invoke-PackageExtension - Collection mode' { + BeforeAll { + $script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "pkg-col-test-$([guid]::NewGuid().ToString('N').Substring(0,8))" + $script:extDir = Join-Path $script:testRoot 'extension' + $script:repoRoot = Join-Path $script:testRoot 'repo' + } + + BeforeEach { + New-Item -Path $script:extDir -ItemType Directory -Force | Out-Null + New-Item -Path $script:repoRoot -ItemType Directory -Force | Out-Null + New-Item -Path (Join-Path $script:repoRoot '.github') -ItemType Directory -Force | Out-Null + New-Item -Path (Join-Path $script:repoRoot '.github/skills') -ItemType Directory -Force | Out-Null + New-Item -Path (Join-Path $script:repoRoot 'scripts/dev-tools') -ItemType Directory -Force | Out-Null + New-Item -Path (Join-Path $script:repoRoot 'scripts/lib/Modules') -ItemType Directory -Force | Out-Null + Set-Content -Path (Join-Path $script:repoRoot 'scripts/lib/Modules/CIHelpers.psm1') -Value '# Mock module' + New-Item -Path (Join-Path $script:repoRoot 'docs/templates') -ItemType Directory -Force | Out-Null + + $manifest = @{ + name = 'test-ext' + version = '1.0.0' + publisher = 'test' + engines = @{ vscode = '^1.80.0' } + contributes = @{} + } + $manifest | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $script:extDir 'package.json') + Set-Content -Path (Join-Path $script:extDir 'README.md') -Value '# Default README' + } + + AfterEach { + if (Test-Path $script:testRoot) { + Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue + } + } + + It 'Uses collection-filtered artifact copy when Collection specified' { + Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } } + Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } } + + $collectionPath = Join-Path $script:testRoot 'collection.json' + @{ id = 'developer'; name = 'dev'; displayName = 'Developer'; personas = @('developer') } | ConvertTo-Json | Set-Content $collectionPath + + $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix' + Set-Content -Path $vsixPath -Value 'fake-vsix' + + $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -Collection $collectionPath + $result | Should -BeOfType [hashtable] + } + + It 'Swaps persona README when collection has matching persona README' { + Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } } + Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } } + + $collectionPath = Join-Path $script:testRoot 'collection.json' + @{ id = 'developer'; name = 'dev'; displayName = 'Developer'; personas = @('developer') } | ConvertTo-Json | Set-Content $collectionPath + + # Create persona README in extension directory + Set-Content -Path (Join-Path $script:extDir 'README.developer.md') -Value '# Developer Persona' + + $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix' + Set-Content -Path $vsixPath -Value 'fake-vsix' + + $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot -Collection $collectionPath + + # README should be restored after packaging completes + $readmeContent = Get-Content -Path (Join-Path $script:extDir 'README.md') -Raw + $readmeContent | Should -Match 'Default README' + $result | Should -BeOfType [hashtable] + } + + It 'Returns failure when no vsix file generated after successful vsce command' { + Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } } + Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } } + + $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot + + $result.Success | Should -BeFalse + $result.ErrorMessage | Should -Match 'No .vsix file found after packaging' + } +} + Describe 'CI Integration - Package-Extension' { BeforeAll { $script:testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "ci-int-test-$([guid]::NewGuid().ToString('N').Substring(0,8))" From 494a7fc18b9465816ed2184141e04ecff30ba272 Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 11:26:23 +0100 Subject: [PATCH 31/62] feat(extension): linting updates --- .cspell.json | 5 +++-- .github/agents/hve-core-installer.agent.md | 8 ++++---- extension/README.developer.md | 7 +++++++ scripts/linting/Validate-MarkdownFrontmatter.ps1 | 1 + 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.cspell.json b/.cspell.json index d5059710..d376e754 100644 --- a/.cspell.json +++ b/.cspell.json @@ -78,6 +78,7 @@ "Streamlit", "vscodeignore", "\u02c8pr\u00e6ks\u026as", - "\u03c0\u03c1\u1fb6\u03be\u03b9\u03c2" + "\u03c0\u03c1\u1fb6\u03be\u03b9\u03c2", + "TMPVER" ] -} +} \ No newline at end of file diff --git a/.github/agents/hve-core-installer.agent.md b/.github/agents/hve-core-installer.agent.md index 401884e1..46c55f9a 100644 --- a/.github/agents/hve-core-installer.agent.md +++ b/.github/agents/hve-core-installer.agent.md @@ -1085,11 +1085,11 @@ User input handling: When the user selects option 2, read the artifact registry to present available personas. -**Step 1: Read registry and build persona agent counts** +#### Step 1: Read registry and build persona agent counts Read `.github/ai-artifacts-registry.json` from the HVE-Core source (at `$hveCoreBasePath`). Parse `personas.definitions` for display names and descriptions. For each agent entry, count stable agents per persona (exclude `experimental` and `deprecated` maturity). -**Step 2: Present persona options** +#### Step 2: Present persona options ```text @@ -1119,7 +1119,7 @@ User input handling: * Persona name (e.g., "developer") โ†’ Match by identifier * Unclear response โ†’ Ask for clarification -**Step 3: Build filtered agent list** +#### Step 3: Build filtered agent list For each selected persona identifier: @@ -1127,7 +1127,7 @@ For each selected persona identifier: 2. Include agents where `maturity` is `stable` AND `personas` array contains the selected persona identifier OR `hve-core-all` 3. Deduplicate across multiple selected personas -**Step 4: Present filtered agents for confirmation** +#### Step 4: Present filtered agents for confirmation ```text diff --git a/extension/README.developer.md b/extension/README.developer.md index ee072a3a..91b8a283 100644 --- a/extension/README.developer.md +++ b/extension/README.developer.md @@ -100,3 +100,10 @@ For issues, questions, or contributions, please visit the [GitHub repository](ht --- Brought to you by Microsoft ISE HVE Essentials + +--- + + +*๐Ÿค– Crafted with precision by โœจCopilot following brilliant human instruction, +then carefully refined by our team of discerning human reviewers.* + diff --git a/scripts/linting/Validate-MarkdownFrontmatter.ps1 b/scripts/linting/Validate-MarkdownFrontmatter.ps1 index 9d285ce9..78778b1e 100644 --- a/scripts/linting/Validate-MarkdownFrontmatter.ps1 +++ b/scripts/linting/Validate-MarkdownFrontmatter.ps1 @@ -30,6 +30,7 @@ param( [string[]]$ExcludePaths = @( 'scripts/tests/Fixtures/**', 'extension/README.md', + 'extension/README.*.md', 'pr.md', '.github/PULL_REQUEST_TEMPLATE.md' ), From 8d96976da198e24ead973827e937601ab23a25fd Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 11:45:31 +0100 Subject: [PATCH 32/62] fix: linting table format --- docs/architecture/ai-artifacts.md | 10 +++--- docs/contributing/ai-artifacts-common.md | 4 +-- extension/PACKAGING.md | 18 +++++----- extension/README.developer.md | 46 ++++++++++++------------ 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/docs/architecture/ai-artifacts.md b/docs/architecture/ai-artifacts.md index 4741a568..addc9033 100644 --- a/docs/architecture/ai-artifacts.md +++ b/docs/architecture/ai-artifacts.md @@ -262,10 +262,10 @@ Each artifact entry contains metadata for filtering and dependency resolution: Personas represent user roles that consume artifacts. The registry defines these personas: -| Persona | Identifier | Target Users | -|----------------------|--------------------|------------------------------------| -| **All** | `hve-core-all` | Universal inclusion | -| **Developer** | `developer` | Software engineers | +| Persona | Identifier | Target Users | +|---------------|----------------|---------------------| +| **All** | `hve-core-all` | Universal inclusion | +| **Developer** | `developer` | Software engineers | Artifacts assigned to `hve-core-all` appear in the full collection and may also include role-specific personas for targeted distribution. @@ -298,7 +298,7 @@ The build system resolves collections by: Collections carry their own maturity level, independent of artifact-level maturity. This controls whether the entire collection is built for a given release channel: | Collection Maturity | PreRelease Channel | Stable Channel | -| ------------------- | ------------------ | -------------- | +|---------------------|--------------------|----------------| | `stable` | Included | Included | | `preview` | Included | Included | | `experimental` | Included | Excluded | diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 345210dc..70e3d898 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -216,7 +216,7 @@ Some artifacts require other artifacts to function correctly. The `requires` fie | Type | Purpose | |----------------|----------------------------------------------------------------------------------| -| `agents` | Agents this artifact dispatches at runtime via `runSubagent` (excludes handoffs) | +| `agents` | Agents this artifact dispatches at runtime via `runSubagent` (excludes handoffs) | | `prompts` | Prompts this artifact invokes or references | | `instructions` | Instructions this artifact relies on for code generation | | `skills` | Skills this artifact executes for specialized tasks | @@ -228,7 +228,7 @@ Some artifacts require other artifacts to function correctly. The `requires` fie Handoff targets and `requires` dependencies follow different maturity rules during extension packaging: | Mechanism | Maturity Filtered | Reason | -| ---------- | ----------------- | ------------------------------------------------------------------------- | +|------------|-------------------|---------------------------------------------------------------------------| | `requires` | Yes | Runtime dependencies are excluded when their maturity exceeds the channel | | `handoffs` | No | UI buttons must resolve to a valid agent or the button is broken | diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index b60ea542..7afd3bde 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -367,7 +367,7 @@ Collection manifests are defined in `extension/collections/`: Each persona collection has a corresponding `package.{collection-id}.json` template file in `extension/`. These files contain static metadata (name, display name, description, publisher) for the persona edition. The `contributes` section is empty because `Prepare-Extension.ps1` populates it dynamically at build time. | Template | Collection | Purpose | -| ------------------------ | ---------- | --------------------------------- | +|--------------------------|------------|-----------------------------------| | `package.json` | Full | Canonical manifest (hve-core-all) | | `package.developer.json` | Developer | Developer edition metadata | @@ -511,21 +511,21 @@ Collection manifests follow this structure: } ``` -| Field | Required | Description | -|---------------|----------|-----------------------------------------------------------------------| -| `id` | Yes | Unique identifier for the collection | -| `name` | Yes | Extension package name | -| `displayName` | Yes | Marketplace display name | -| `description` | Yes | Marketplace description text | +| Field | Required | Description | +|---------------|----------|-------------------------------------------------------------------------------------------------------| +| `id` | Yes | Unique identifier for the collection | +| `name` | Yes | Extension package name | +| `displayName` | Yes | Marketplace display name | +| `description` | Yes | Marketplace description text | | `maturity` | No | Release channel eligibility (`stable`, `preview`, `experimental`, `deprecated`). Defaults to `stable` | -| `personas` | Yes | Array of persona identifiers to include | +| `personas` | Yes | Array of persona identifiers to include | #### Collection Maturity and Channel Eligibility The `maturity` field controls which release channels include the collection: | Collection Maturity | PreRelease Channel | Stable Channel | -| ------------------- | ------------------ | -------------- | +|---------------------|--------------------|----------------| | `stable` | Yes | Yes | | `preview` | Yes | Yes | | `experimental` | Yes | No | diff --git a/extension/README.developer.md b/extension/README.developer.md index 91b8a283..27fcec8d 100644 --- a/extension/README.developer.md +++ b/extension/README.developer.md @@ -8,37 +8,37 @@ HVE Core - Developer Edition provides a focused collection of AI chat agents, pr ### ๐Ÿค– Chat Agents -| Agent | Description | -| ----- | ----------- | -| **memory** | Conversation memory persistence for session continuity | -| **rpi-agent** | Autonomous RPI orchestrator dispatching task agents through Research, Plan, Implement, Review, and Discover phases | -| **task-implementor** | Executes implementation plans with progressive tracking and change records | -| **task-planner** | Implementation planner for creating actionable implementation plans | -| **task-researcher** | Task research specialist for comprehensive project analysis | -| **task-reviewer** | Reviews completed implementation work for accuracy, completeness, and convention compliance | +| Agent | Description | +|----------------------|--------------------------------------------------------------------------------------------------------------------| +| **memory** | Conversation memory persistence for session continuity | +| **rpi-agent** | Autonomous RPI orchestrator dispatching task agents through Research, Plan, Implement, Review, and Discover phases | +| **task-implementor** | Executes implementation plans with progressive tracking and change records | +| **task-planner** | Implementation planner for creating actionable implementation plans | +| **task-researcher** | Task research specialist for comprehensive project analysis | +| **task-reviewer** | Reviews completed implementation work for accuracy, completeness, and convention compliance | ### ๐Ÿ“ Prompts -| Prompt | Description | -| ------ | ----------- | -| **checkpoint** | Save or restore conversation context using memory files | -| **rpi** | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks | -| **task-implement** | Locates and executes implementation plans using task-implementor mode | -| **task-plan** | Initiates implementation planning based on user context or research documents | -| **task-research** | Initiates research for implementation planning based on user requirements | -| **task-review** | Initiates implementation review based on user context or automatic artifact discovery | +| Prompt | Description | +|--------------------|---------------------------------------------------------------------------------------| +| **checkpoint** | Save or restore conversation context using memory files | +| **rpi** | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks | +| **task-implement** | Locates and executes implementation plans using task-implementor mode | +| **task-plan** | Initiates implementation planning based on user context or research documents | +| **task-research** | Initiates research for implementation planning based on user requirements | +| **task-review** | Initiates implementation review based on user context or automatic artifact discovery | ### ๐Ÿ“š Instructions -| Instruction | Description | -| ----------- | ----------- | -| **commit-message** | Required instructions for creating all commit messages | -| **markdown** | Required instructions for creating or editing any Markdown files | +| Instruction | Description | +|--------------------|------------------------------------------------------------------| +| **commit-message** | Required instructions for creating all commit messages | +| **markdown** | Required instructions for creating or editing any Markdown files | ### โšก Skills -| Skill | Description | -| ----- | ----------- | +| Skill | Description | +|------------------|-----------------------------------------------------------| | **video-to-gif** | Video-to-GIF conversion with FFmpeg two-pass optimization | ## Getting Started @@ -72,7 +72,7 @@ Prompts are available in the Copilot Chat prompt picker and can be used to gener HVE Core offers two installation channels: | Channel | Description | Maturity Levels | -| ----------- | ------------------------------------------------------- | ----------------------------------- | +|-------------|---------------------------------------------------------|-------------------------------------| | Stable | Production-ready artifacts only | `stable` | | Pre-release | Early access to new features and experimental artifacts | `stable`, `preview`, `experimental` | From 603c4eeb0436e351a6e36884965554884d793f9a Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 10:51:54 +0000 Subject: [PATCH 33/62] refactor(extension): remove dead code and align workflow collection matrices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - remove unused Get-ExtensionOutputPath function and its tests - remove redundant .github/skills directory spec (already covered by .github) - align main.yml and extension-publish.yml to consume filtered matrix from extension-package.yml - eliminate discover-release-collections and discover-vsix jobs that ignored maturity filtering ๐Ÿ”ง - Generated by Copilot --- .github/workflows/extension-package.yml | 3 ++ .github/workflows/extension-publish.yml | 32 +------------- .github/workflows/main.yml | 28 +----------- scripts/extension/Package-Extension.ps1 | 44 ------------------- .../extension/Package-Extension.Tests.ps1 | 42 +----------------- 5 files changed, 9 insertions(+), 140 deletions(-) diff --git a/.github/workflows/extension-package.yml b/.github/workflows/extension-package.yml index 429fa5a1..d0bb3f1e 100644 --- a/.github/workflows/extension-package.yml +++ b/.github/workflows/extension-package.yml @@ -27,6 +27,9 @@ on: version: description: 'Version that was packaged' value: ${{ jobs.package.outputs.version }} + collections-matrix: + description: 'JSON matrix of collections that were actually packaged (filtered by channel and maturity)' + value: ${{ jobs.discover-collections.outputs.matrix }} permissions: contents: read diff --git a/.github/workflows/extension-publish.yml b/.github/workflows/extension-publish.yml index 4a3f5713..7840a2df 100644 --- a/.github/workflows/extension-publish.yml +++ b/.github/workflows/extension-publish.yml @@ -83,43 +83,15 @@ jobs: permissions: contents: read - discover-vsix: - name: Discover VSIX Artifacts - needs: [package] - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.discover.outputs.matrix }} - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 - with: - persist-credentials: false - - - name: Discover collection artifacts - id: discover - shell: bash - run: | - # Build publish matrix from collection manifests - collections_dir="extension/collections" - matrix_json=$(find "$collections_dir" -name '*.collection.json' -type f | sort | while IFS= read -r file; do - id=$(jq -r '.id' "$file") - name=$(jq -r '.name' "$file") - echo "{\"id\":\"$id\",\"name\":\"$name\"}" - done | jq -s '{include: .}') - - echo "matrix=$matrix_json" >> "$GITHUB_OUTPUT" - echo "Discovered collections for publish:" - echo "$matrix_json" | jq . - publish: name: Publish ${{ matrix.id }} - needs: [package, discover-vsix] + needs: [package] if: ${{ !inputs.dry-run }} runs-on: ubuntu-latest environment: marketplace strategy: fail-fast: false - matrix: ${{ fromJson(needs.discover-vsix.outputs.matrix) }} + matrix: ${{ fromJson(needs.package.outputs.collections-matrix) }} permissions: contents: read id-token: write diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fb6129f0..dfd3b2a9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -106,38 +106,14 @@ jobs: permissions: contents: read - discover-release-collections: - name: Discover Release Collections - needs: [release-please, extension-package-release] - if: ${{ needs.release-please.outputs.release_created == 'true' }} - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.discover.outputs.matrix }} - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 - with: - persist-credentials: false - - - name: Discover collection manifests - id: discover - shell: bash - run: | - collections_dir="extension/collections" - matrix_json=$(find "$collections_dir" -name '*.collection.json' -type f | sort | while IFS= read -r file; do - id=$(jq -r '.id' "$file") - echo "{\"id\":\"$id\"}" - done | jq -s '{include: .}') - echo "matrix=$matrix_json" >> "$GITHUB_OUTPUT" - attest-and-upload: name: Attest and Upload (${{ matrix.id }}) - needs: [release-please, extension-package-release, discover-release-collections] + needs: [release-please, extension-package-release] if: ${{ needs.release-please.outputs.release_created == 'true' }} runs-on: ubuntu-latest strategy: fail-fast: false - matrix: ${{ fromJson(needs.discover-release-collections.outputs.matrix) }} + matrix: ${{ fromJson(needs.extension-package-release.outputs.collections-matrix) }} permissions: contents: write id-token: write diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index c3aaa112..3dfbd2f1 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -121,45 +121,6 @@ function Test-VsceAvailable { } } -function Get-ExtensionOutputPath { - <# - .SYNOPSIS - Constructs the expected .vsix output path from extension directory and version. - .PARAMETER ExtensionDirectory - The path to the extension directory. - .PARAMETER ExtensionName - The name of the extension (from package.json). - .PARAMETER PackageVersion - The version string to use in the filename. - .PARAMETER CollectionId - Optional collection identifier to use instead of the extension name in the filename. - .OUTPUTS - String path to the expected .vsix file. - #> - [CmdletBinding()] - [OutputType([string])] - param( - [Parameter(Mandatory = $true)] - [string]$ExtensionDirectory, - - [Parameter(Mandatory = $true)] - [string]$ExtensionName, - - [Parameter(Mandatory = $true)] - [string]$PackageVersion, - - [Parameter(Mandatory = $false)] - [string]$CollectionId = "" - ) - - $vsixFileName = if ($CollectionId -and $CollectionId -ne "") { - "$CollectionId-$PackageVersion.vsix" - } else { - "$ExtensionName-$PackageVersion.vsix" - } - return Join-Path $ExtensionDirectory $vsixFileName -} - function Test-ExtensionManifestValid { <# .SYNOPSIS @@ -494,11 +455,6 @@ function Get-PackagingDirectorySpec { Source = Join-Path $RepoRoot "docs/templates" Destination = Join-Path $ExtensionDirectory "docs/templates" IsFile = $false - }, - @{ - Source = Join-Path $RepoRoot ".github/skills" - Destination = Join-Path $ExtensionDirectory ".github/skills" - IsFile = $false } ) } diff --git a/scripts/tests/extension/Package-Extension.Tests.ps1 b/scripts/tests/extension/Package-Extension.Tests.ps1 index 7709d623..35c355bc 100644 --- a/scripts/tests/extension/Package-Extension.Tests.ps1 +++ b/scripts/tests/extension/Package-Extension.Tests.ps1 @@ -62,36 +62,6 @@ Describe 'Test-VsceAvailable' { } } -Describe 'Get-ExtensionOutputPath' { - BeforeAll { - $script:testDir = [System.IO.Path]::GetTempPath().TrimEnd([System.IO.Path]::DirectorySeparatorChar) - } - - It 'Constructs correct output path' { - $result = Get-ExtensionOutputPath -ExtensionDirectory $script:testDir -ExtensionName 'my-extension' -PackageVersion '1.0.0' - $expected = [System.IO.Path]::Combine($script:testDir, 'my-extension-1.0.0.vsix') - $result | Should -Be $expected - } - - It 'Handles pre-release version numbers' { - $result = Get-ExtensionOutputPath -ExtensionDirectory $script:testDir -ExtensionName 'ext' -PackageVersion '2.1.0-preview.1' - $expected = [System.IO.Path]::Combine($script:testDir, 'ext-2.1.0-preview.1.vsix') - $result | Should -Be $expected - } - - It 'Uses collection ID in filename when specified' { - $result = Get-ExtensionOutputPath -ExtensionDirectory $script:testDir -ExtensionName 'my-extension' -PackageVersion '1.0.0' -CollectionId 'developer' - $expected = [System.IO.Path]::Combine($script:testDir, 'developer-1.0.0.vsix') - $result | Should -Be $expected - } - - It 'Uses extension name when no collection ID' { - $result = Get-ExtensionOutputPath -ExtensionDirectory $script:testDir -ExtensionName 'my-extension' -PackageVersion '1.0.0' -CollectionId '' - $expected = [System.IO.Path]::Combine($script:testDir, 'my-extension-1.0.0.vsix') - $result | Should -Be $expected - } -} - Describe 'Test-ExtensionManifestValid' { It 'Returns valid result for proper manifest' { $manifest = [PSCustomObject]@{ @@ -697,9 +667,9 @@ Describe 'Get-PackagingDirectorySpec' { $script:extDir = Join-Path ([System.IO.Path]::GetTempPath()) 'spec-ext' } - It 'Returns array of 5 directory specifications' { + It 'Returns array of 4 directory specifications' { $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir - $result.Count | Should -Be 5 + $result.Count | Should -Be 4 } It 'Includes .github directory specification' { @@ -731,14 +701,6 @@ Describe 'Get-PackagingDirectorySpec' { $templatesSpec.IsFile | Should -BeFalse } - It 'Includes skills directory specification' { - $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir - $skillsSpec = $result | Where-Object { $_.Source -like '*skills' } - $skillsSpec | Should -Not -BeNullOrEmpty - $skillsSpec.Destination | Should -BeLike '*skills' - $skillsSpec.IsFile | Should -BeFalse - } - It 'Uses correct path joining for source and destination' { $result = Get-PackagingDirectorySpec -RepoRoot $script:repoRoot -ExtensionDirectory $script:extDir foreach ($spec in $result) { From 3cb3a96d9c87a4e0b9fe62d9b8b21474b96e7c13 Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 12:44:34 +0100 Subject: [PATCH 34/62] fix(agents): update agent description format to include tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit style(extension): correct punctuation in README instructions fix(scripts): improve error handling for missing collection artifacts ๐Ÿ”ง - Generated by Copilot --- .github/agents/hve-core-installer.agent.md | 4 ++-- extension/README.developer.md | 2 +- scripts/extension/Package-Extension.ps1 | 26 +++++++++++++--------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.github/agents/hve-core-installer.agent.md b/.github/agents/hve-core-installer.agent.md index 46c55f9a..4cfbfc57 100644 --- a/.github/agents/hve-core-installer.agent.md +++ b/.github/agents/hve-core-installer.agent.md @@ -1135,8 +1135,8 @@ For each selected persona identifier: The following [N] agents will be copied: - โ€ข [agent-name-1] - [agent description from registry tags] - โ€ข [agent-name-2] - [agent description from registry tags] + โ€ข [agent-name-1] - tags: [tag-1, tag-2] + โ€ข [agent-name-2] - tags: [tag-1, tag-2] ... Proceed with installation? (yes/no) diff --git a/extension/README.developer.md b/extension/README.developer.md index 27fcec8d..f3d112c9 100644 --- a/extension/README.developer.md +++ b/extension/README.developer.md @@ -47,7 +47,7 @@ After installing this extension, the chat agents will be available in GitHub Cop 1. **Use custom agents** by selecting the custom agent from the agent picker drop-down list in Copilot Chat 2. **Apply prompts** through the Copilot Chat interface -3. **Reference instructions** โ€” They're automatically applied based on file patterns +3. **Reference instructions**: they're automatically applied based on file patterns ### Post-Installation Setup diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index 3dfbd2f1..e3a64604 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -499,9 +499,10 @@ function Copy-CollectionArtifacts { New-Item -Path $agentsDestDir -ItemType Directory -Force | Out-Null foreach ($agent in $preparedPkgJson.contributes.chatAgents) { $srcPath = Join-Path $RepoRoot ($agent.path -replace '^\.[\\/]', '') - if (Test-Path $srcPath) { - Copy-Item -Path $srcPath -Destination $agentsDestDir -Force + if (-not (Test-Path $srcPath)) { + throw "Collection artifact not found: $srcPath (referenced by contributes.chatAgents in package.json)" } + Copy-Item -Path $srcPath -Destination $agentsDestDir -Force } } @@ -509,12 +510,13 @@ function Copy-CollectionArtifacts { if ($preparedPkgJson.contributes.chatPromptFiles) { foreach ($prompt in $preparedPkgJson.contributes.chatPromptFiles) { $srcPath = Join-Path $RepoRoot ($prompt.path -replace '^\.[\\/]', '') + if (-not (Test-Path $srcPath)) { + throw "Collection artifact not found: $srcPath (referenced by contributes.chatPromptFiles in package.json)" + } $destPath = Join-Path $ExtensionDirectory ($prompt.path -replace '^\.[\\/]', '') $destDir = Split-Path $destPath -Parent New-Item -Path $destDir -ItemType Directory -Force | Out-Null - if (Test-Path $srcPath) { - Copy-Item -Path $srcPath -Destination $destPath -Force - } + Copy-Item -Path $srcPath -Destination $destPath -Force } } @@ -522,12 +524,13 @@ function Copy-CollectionArtifacts { if ($preparedPkgJson.contributes.chatInstructions) { foreach ($instr in $preparedPkgJson.contributes.chatInstructions) { $srcPath = Join-Path $RepoRoot ($instr.path -replace '^\.[\\/]', '') + if (-not (Test-Path $srcPath)) { + throw "Collection artifact not found: $srcPath (referenced by contributes.chatInstructions in package.json)" + } $destPath = Join-Path $ExtensionDirectory ($instr.path -replace '^\.[\\/]', '') $destDir = Split-Path $destPath -Parent New-Item -Path $destDir -ItemType Directory -Force | Out-Null - if (Test-Path $srcPath) { - Copy-Item -Path $srcPath -Destination $destPath -Force - } + Copy-Item -Path $srcPath -Destination $destPath -Force } } @@ -535,12 +538,13 @@ function Copy-CollectionArtifacts { if ($preparedPkgJson.contributes.chatSkills) { foreach ($skill in $preparedPkgJson.contributes.chatSkills) { $srcPath = Join-Path $RepoRoot ($skill.path -replace '^\.[\\/]', '') + if (-not (Test-Path $srcPath)) { + throw "Collection artifact not found: $srcPath (referenced by contributes.chatSkills in package.json)" + } $destPath = Join-Path $ExtensionDirectory ($skill.path -replace '^\.[\\/]', '') $destDir = Split-Path $destPath -Parent New-Item -Path $destDir -ItemType Directory -Force | Out-Null - if (Test-Path $srcPath) { - Copy-Item -Path $srcPath -Destination $destPath -Recurse -Force - } + Copy-Item -Path $srcPath -Destination $destPath -Recurse -Force } } } From 191fb813c94c2af9b6b518df9cd5c788ad4ae7a3 Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 13:42:15 +0100 Subject: [PATCH 35/62] feat(agents): update installation options and persona selection process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - change default installation option to RPI Core only - simplify persona selection prompt and update agent count descriptions - refine user input handling for agent installation choices ๐Ÿ”ง - Generated by Copilot --- .github/agents/hve-core-installer.agent.md | 41 +++++++++------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/.github/agents/hve-core-installer.agent.md b/.github/agents/hve-core-installer.agent.md index 4cfbfc57..a17e22b3 100644 --- a/.github/agents/hve-core-installer.agent.md +++ b/.github/agents/hve-core-installer.agent.md @@ -1064,21 +1064,19 @@ Copying agents enables local customization and offline use. โ€ข github-backlog-manager (GitHub) Options: - [1] Install all agents (recommended) + [1] Install RPI Core only (recommended) [2] Install by persona collection - [3] Install RPI Core only - [4] Skip agent installation + [3] Skip agent installation -Your choice? (1/2/3/4) +Your choice? (1/2/3) ``` User input handling: -* "1", "all", "install all" โ†’ Copy all stable agents +* "1", "rpi", "rpi core", "core" โ†’ Copy RPI Core bundle only * "2", "persona", "collection", "by persona" โ†’ Proceed to Persona Selection sub-flow -* "3", "rpi", "rpi core", "core" โ†’ Copy RPI Core bundle only -* "4", "skip", "none", "no" โ†’ Skip to success report +* "3", "skip", "none", "no" โ†’ Skip to success report * Unclear response โ†’ Ask for clarification ### Persona Selection Sub-Flow @@ -1095,22 +1093,17 @@ Read `.github/ai-artifacts-registry.json` from the HVE-Core source (at `$hveCore ```text ๐ŸŽญ Persona Collection Selection -Choose one or more personas to install agents tailored to your role. -Agents marked for all personas are always included. +Choose one or more personas to install agents tailored to your role, more to come in the future. | # | Persona | Agents | Description | |---|--------------------|--------|----------------------------------| | 1 | Developer | [N] | Software engineers writing code | -| 2 | TPM | [N] | Program/product managers | -| 3 | DevOps Engineer | [N] | Platform, SRE, infrastructure | -| 4 | Architect | [N] | Solution/system architects | -| 5 | Technical Writer | [N] | Documentation specialists | -Enter persona number(s) separated by commas (e.g., "1, 3"): +Enter persona number(s) separated by commas (e.g., "1"): ``` -Agent counts `[N]` include agents matching the persona plus all `hve-core-all` agents with `stable` maturity. +Agent counts `[N]` include agents matching the persona with `stable` maturity. User input handling: @@ -1124,7 +1117,7 @@ User input handling: For each selected persona identifier: 1. Iterate through `agents` in the registry -2. Include agents where `maturity` is `stable` AND `personas` array contains the selected persona identifier OR `hve-core-all` +2. Include agents where `maturity` is `stable` AND `personas` array contains the selected persona identifier 3. Deduplicate across multiple selected personas #### Step 4: Present filtered agents for confirmation @@ -1156,15 +1149,14 @@ User input handling: | Bundle | Agents | | ------ | ------ | -| `rpi-core` | task-researcher, task-planner, task-implementor, rpi-agent | -| `all` | All stable agents from the registry | -| `persona:` | Stable agents matching the persona plus all `hve-core-all` agents | +| `rpi-core` | task-researcher, task-planner, task-implementor, task-reviewer, rpi-agent | +| `persona:` | Stable agents matching the persona | ### Collision Detection Before copying, check for existing agent files with matching names. Generate a script for the user's shell that: -1. Builds list of source files based on selection (`rpi-core` = 4 files, `all` = all `.agent.md` files) +1. Builds list of source files based on selection (`rpi-core` = 5 files, `persona` = filtered `.agent.md` files) 2. Copies files with `.agent.md` extension 3. Checks target directory (`.github/agents/`) for each name 4. Reports collisions or clean state @@ -1178,8 +1170,7 @@ $targetDir = ".github/agents" # Get files to copy based on selection $filesToCopy = switch ($selection) { - "rpi-core" { @("task-researcher.agent.md", "task-planner.agent.md", "task-implementor.agent.md", "rpi-agent.agent.md") } - "all" { Get-ChildItem "$sourceDir/*.agent.md" | ForEach-Object { $_.Name } } + "rpi-core" { @("task-researcher.agent.md", "task-planner.agent.md", "task-implementor.agent.md", "task-reviewer.agent.md", "rpi-agent.agent.md") } default { # Persona-based: $selection contains filtered agent names from registry $personaAgents @@ -1202,7 +1193,7 @@ if ($collisions.Count -gt 0) { ``` -Bash adaptation: Use `case/esac` for selection, `find ... -name '*.agent.md' -exec basename {} \;` for `all` (portable across GNU/BSD), `test -f` for existence. +Bash adaptation: Use `case/esac` for selection, `test -f` for existence checks. ### Collision Resolution Prompt @@ -1265,7 +1256,7 @@ $manifest = @{ source = "microsoft/hve-core" version = (Get-Content "$hveCoreBasePath/package.json" | ConvertFrom-Json).version installed = (Get-Date -Format "o") - collection = $collectionId # "hve-core-all", "rpi-core", or persona id(s) e.g. "developer" or "developer,devops" + collection = $collectionId # "rpi-core" or persona id(s) e.g. "developer" or "developer,devops" files = @{}; skip = @() } @@ -1342,7 +1333,7 @@ if (Test-Path $manifestPath) { Write-Host "INSTALLED_VERSION=$($manifest.version)" Write-Host "SOURCE_VERSION=$sourceVersion" Write-Host "VERSION_CHANGED=$($sourceVersion -ne $manifest.version)" - Write-Host "INSTALLED_COLLECTION=$($manifest.collection ?? 'hve-core-all')" + Write-Host "INSTALLED_COLLECTION=$($manifest.collection ?? 'rpi-core')" } else { Write-Host "UPGRADE_MODE=false" } From 0a39f435b51ff232f4a156b7cb23c014dd3c59b9 Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 13:54:20 +0100 Subject: [PATCH 36/62] fix(scripts): skip missing collection artifacts instead of throwing errors --- scripts/extension/Package-Extension.ps1 | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index e3a64604..1423a8ec 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -500,7 +500,8 @@ function Copy-CollectionArtifacts { foreach ($agent in $preparedPkgJson.contributes.chatAgents) { $srcPath = Join-Path $RepoRoot ($agent.path -replace '^\.[\\/]', '') if (-not (Test-Path $srcPath)) { - throw "Collection artifact not found: $srcPath (referenced by contributes.chatAgents in package.json)" + Write-Warning "Skipping missing collection artifact: $srcPath (referenced by contributes.chatAgents in package.json)" + continue } Copy-Item -Path $srcPath -Destination $agentsDestDir -Force } @@ -511,7 +512,8 @@ function Copy-CollectionArtifacts { foreach ($prompt in $preparedPkgJson.contributes.chatPromptFiles) { $srcPath = Join-Path $RepoRoot ($prompt.path -replace '^\.[\\/]', '') if (-not (Test-Path $srcPath)) { - throw "Collection artifact not found: $srcPath (referenced by contributes.chatPromptFiles in package.json)" + Write-Warning "Skipping missing collection artifact: $srcPath (referenced by contributes.chatPromptFiles in package.json)" + continue } $destPath = Join-Path $ExtensionDirectory ($prompt.path -replace '^\.[\\/]', '') $destDir = Split-Path $destPath -Parent @@ -525,7 +527,8 @@ function Copy-CollectionArtifacts { foreach ($instr in $preparedPkgJson.contributes.chatInstructions) { $srcPath = Join-Path $RepoRoot ($instr.path -replace '^\.[\\/]', '') if (-not (Test-Path $srcPath)) { - throw "Collection artifact not found: $srcPath (referenced by contributes.chatInstructions in package.json)" + Write-Warning "Skipping missing collection artifact: $srcPath (referenced by contributes.chatInstructions in package.json)" + continue } $destPath = Join-Path $ExtensionDirectory ($instr.path -replace '^\.[\\/]', '') $destDir = Split-Path $destPath -Parent @@ -539,7 +542,8 @@ function Copy-CollectionArtifacts { foreach ($skill in $preparedPkgJson.contributes.chatSkills) { $srcPath = Join-Path $RepoRoot ($skill.path -replace '^\.[\\/]', '') if (-not (Test-Path $srcPath)) { - throw "Collection artifact not found: $srcPath (referenced by contributes.chatSkills in package.json)" + Write-Warning "Skipping missing collection artifact: $srcPath (referenced by contributes.chatSkills in package.json)" + continue } $destPath = Join-Path $ExtensionDirectory ($skill.path -replace '^\.[\\/]', '') $destDir = Split-Path $destPath -Parent From 8e02d0e214673400763b9a60cbca9e101642c00a Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 13:56:23 +0100 Subject: [PATCH 37/62] fix(scripts): add warnings as errors for artifact registry linting --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6adc54b6..2f7cdd20 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "lint:links": "pwsh -NoProfile -Command \"& scripts/linting/Invoke-LinkLanguageCheck.ps1 -ExcludePaths 'scripts/tests/**'\"", "lint:md-links": "pwsh -File scripts/linting/Markdown-Link-Check.ps1", "lint:frontmatter": "pwsh -NoProfile -Command \"& './scripts/linting/Validate-MarkdownFrontmatter.ps1' -WarningsAsErrors -EnableSchemaValidation\"", - "lint:registry": "pwsh -NoProfile -Command \"& './scripts/linting/Validate-ArtifactRegistry.ps1'\"", + "lint:registry": "pwsh -NoProfile -Command \"& './scripts/linting/Validate-ArtifactRegistry.ps1' -WarningsAsErrors\"", "lint:version-consistency": "pwsh -NoProfile -Command \"./scripts/security/Test-ActionVersionConsistency.ps1 -FailOnMismatch\"", "lint:all": "npm run format:tables && npm run lint:md && npm run lint:ps && npm run lint:yaml && npm run lint:links && npm run lint:frontmatter && npm run lint:registry && npm run lint:version-consistency", "format:tables": "markdown-table-formatter \"**/*.md\"", From 9b6b82715f3894044920d44af1e8a2deb091b3bd Mon Sep 17 00:00:00 2001 From: katriendg Date: Tue, 10 Feb 2026 14:23:04 +0100 Subject: [PATCH 38/62] feat(workflows): update version output in extension packaging workflow - change version output to use discover-collections job - add step to read package version from package.json - enhance dependency resolution documentation for clarity --- .github/workflows/extension-package.yml | 10 +++++++--- docs/contributing/ai-artifacts-common.md | 7 ++----- docs/getting-started/install.md | 11 +++++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/extension-package.yml b/.github/workflows/extension-package.yml index d0bb3f1e..56257ded 100644 --- a/.github/workflows/extension-package.yml +++ b/.github/workflows/extension-package.yml @@ -26,7 +26,7 @@ on: outputs: version: description: 'Version that was packaged' - value: ${{ jobs.package.outputs.version }} + value: ${{ jobs.discover-collections.outputs.version }} collections-matrix: description: 'JSON matrix of collections that were actually packaged (filtered by channel and maturity)' value: ${{ jobs.discover-collections.outputs.matrix }} @@ -42,12 +42,18 @@ jobs: contents: read outputs: matrix: ${{ steps.discover.outputs.matrix }} + version: ${{ steps.read-version.outputs.version }} steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 with: persist-credentials: false + - name: Read package version + id: read-version + shell: bash + run: echo "version=$(jq -r .version extension/package.json)" >> "$GITHUB_OUTPUT" + - name: Discover collection manifests id: discover shell: bash @@ -97,8 +103,6 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJson(needs.discover-collections.outputs.matrix) }} - outputs: - version: ${{ steps.package.outputs.version }} steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 70e3d898..89e2d768 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -256,12 +256,9 @@ Add the `requires` field to artifacts that depend on others: ### Dependency Resolution -When installing artifacts via clone methods, the installer agent resolves dependencies: +Dependency resolution currently operates at **build time** during extension packaging. The `Resolve-RequiresDependencies` function in `Prepare-Extension.ps1` walks `requires` blocks to compute the transitive closure of all dependent artifacts across types (agents, prompts, instructions, skills). Similarly, `Resolve-HandoffDependencies` performs BFS traversal of agent handoff declarations to ensure all reachable agents are included in the package. -1. User selects artifacts to install (by persona or explicit selection) -2. Installer reads the registry and builds dependency graph -3. Required artifacts are automatically included regardless of persona filter -4. Circular dependencies are detected and reported as validation errors +For clone-based installations, the installer agent supports **agent-only persona filtering** in Phase 7. Full installer-side dependency resolution (automatically including required prompts, instructions, and skills based on the dependency graph) is planned for a future release. ### Dependency Best Practices diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 23917a41..4f704d13 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -89,10 +89,13 @@ Answer these questions to find your recommended installation method: HVE-Core supports persona-based artifact collections tailored to specific roles: -| Collection | Identifier | Description | -|---------------|-----------------|--------------------------------------| -| **Full** | `hve-core` | All artifacts (recommended for most) | -| **Developer** | `hve-developer` | Software engineering focus | +| Collection | Extension Name | Registry ID | Maturity | Description | +|---------------|-----------------|----------------|--------------|--------------------------------------| +| **Full** | `hve-core` | `hve-core-all` | Stable | All artifacts (recommended for most) | +| **Developer** | `hve-developer` | `developer` | Experimental | Software engineering focus | + +> [!NOTE] +> Experimental collections are only available via PreRelease extension builds. The Stable channel includes the Full collection only. ### Extension Installation (Full Collection) From f5a812681b2c7442b1aae227dd5f09a4801ea62c Mon Sep 17 00:00:00 2001 From: katriendg Date: Wed, 11 Feb 2026 09:35:51 +0100 Subject: [PATCH 39/62] feat(extension): add developer persona and collection-based pre-release publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add developer persona to stable artifacts in ai-artifacts-registry - refactor pre-release workflow to use extension-package.yml reusable workflow - update pre-release publish job with collection matrix strategy - document collection-based packaging and artifact-registry-validation in workflows ๐Ÿš€ - Generated by Copilot --- .github/ai-artifacts-registry.json | 123 +++++++++++------ .../extension-publish-prerelease.yml | 83 ++++-------- docs/architecture/workflows.md | 128 ++++++++++-------- 3 files changed, 184 insertions(+), 150 deletions(-) diff --git a/.github/ai-artifacts-registry.json b/.github/ai-artifacts-registry.json index 0edcfb61..60f81d04 100644 --- a/.github/ai-artifacts-registry.json +++ b/.github/ai-artifacts-registry.json @@ -51,7 +51,8 @@ "adr-creation": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "architecture", @@ -67,7 +68,8 @@ "arch-diagram-builder": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "architecture", @@ -83,7 +85,8 @@ "brd-builder": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "planning", @@ -148,7 +151,8 @@ "gen-streamlit-dashboard": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "data-science" @@ -213,7 +217,8 @@ "hve-core-installer": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "tooling" @@ -228,7 +233,8 @@ "memory": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "rpi", @@ -247,7 +253,8 @@ "pr-review": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "github", @@ -263,7 +270,8 @@ "prd-builder": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "planning", @@ -279,7 +287,8 @@ "prompt-builder": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "prompt-engineering" @@ -335,7 +344,8 @@ "security-plan-creator": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "architecture", @@ -351,7 +361,8 @@ "task-implementor": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "rpi", @@ -369,7 +380,8 @@ "task-planner": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "rpi", @@ -385,7 +397,8 @@ "task-researcher": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "rpi", @@ -401,7 +414,8 @@ "task-reviewer": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "rpi", @@ -417,7 +431,8 @@ "test-streamlit-dashboard": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "data-science", @@ -435,7 +450,8 @@ "ado-create-pull-request": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "ado", @@ -482,7 +498,8 @@ "checkpoint": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "rpi", @@ -501,7 +518,8 @@ "git-commit": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "git" @@ -510,7 +528,8 @@ "git-commit-message": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "git" @@ -519,7 +538,8 @@ "git-merge": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "git" @@ -528,7 +548,8 @@ "git-setup": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "git" @@ -596,7 +617,8 @@ "prompt-analyze": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "prompt-engineering" @@ -605,7 +627,8 @@ "prompt-build": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "prompt-engineering" @@ -614,7 +637,8 @@ "prompt-refactor": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "prompt-engineering" @@ -623,7 +647,8 @@ "pull-request": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "github", @@ -656,7 +681,8 @@ "task-implement": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "rpi", @@ -666,7 +692,8 @@ "task-plan": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "rpi", @@ -676,7 +703,8 @@ "task-research": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "rpi", @@ -686,7 +714,8 @@ "task-review": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "rpi", @@ -745,7 +774,8 @@ "commit-message": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "git" @@ -764,7 +794,8 @@ "git-merge": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "git" @@ -813,7 +844,8 @@ "hve-core-location": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "tooling" @@ -836,7 +868,8 @@ "prompt-builder": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "prompt-engineering" @@ -845,7 +878,8 @@ "python-script": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "language" @@ -854,7 +888,8 @@ "uv-projects": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "language" @@ -872,7 +907,8 @@ "bash/bash": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "language" @@ -881,7 +917,8 @@ "bicep/bicep": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "language", @@ -891,7 +928,8 @@ "csharp/csharp": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "language" @@ -900,7 +938,8 @@ "csharp/csharp-tests": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "language", @@ -910,7 +949,8 @@ "terraform/terraform": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "language", @@ -922,7 +962,8 @@ "video-to-gif": { "maturity": "stable", "personas": [ - "hve-core-all" + "hve-core-all", + "developer" ], "tags": [ "media", diff --git a/.github/workflows/extension-publish-prerelease.yml b/.github/workflows/extension-publish-prerelease.yml index da4bab42..2b2d2434 100644 --- a/.github/workflows/extension-publish-prerelease.yml +++ b/.github/workflows/extension-publish-prerelease.yml @@ -50,60 +50,24 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" package: - name: Package Pre-Release Extension + name: Package Pre-Release Extensions needs: validate-version - runs-on: ubuntu-latest - outputs: - version: ${{ steps.package.outputs.version }} - vsix-file: ${{ steps.package.outputs.vsix-file }} - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 - with: - persist-credentials: false - - - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.1.0 - with: - node-version: '20' - - - name: Install VSCE - run: npm install -g @vscode/vsce@3.7.1 - - - name: Setup PowerShell - shell: pwsh - run: | - Write-Host "PowerShell version: $($PSVersionTable.PSVersion)" - Install-Module -Name PowerShell-Yaml -Force -Scope CurrentUser - - - name: Prepare extension resources - id: prepare - shell: pwsh - run: | - Write-Host "๐Ÿ”ง Preparing extension for PreRelease channel..." - ./scripts/extension/Prepare-Extension.ps1 -Channel PreRelease - - - name: Package pre-release extension - id: package - shell: pwsh - run: | - $version = "${{ needs.validate-version.outputs.version }}" - Write-Host "๐Ÿ“ฆ Packaging pre-release extension v$version..." - ./scripts/extension/Package-Extension.ps1 -Version $version -PreRelease - - - name: Upload VSIX artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4.4.3 - with: - name: extension-vsix-prerelease - path: extension/*.vsix - retention-days: 30 + uses: ./.github/workflows/extension-package.yml + with: + version: ${{ needs.validate-version.outputs.version }} + channel: PreRelease + permissions: + contents: read publish: - name: Publish Pre-Release to Marketplace - needs: [validate-version, package] + name: Publish ${{ matrix.id }} + needs: [package] if: ${{ !inputs.dry-run }} runs-on: ubuntu-latest environment: marketplace + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.package.outputs.collections-matrix) }} permissions: contents: read id-token: write @@ -131,25 +95,32 @@ jobs: - name: Download VSIX artifact uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: - name: extension-vsix-prerelease - path: ./extension + name: extension-vsix-${{ matrix.id }} + path: ./dist - name: Publish pre-release to VS Code Marketplace + id: publish run: | - VSIX_FILE=$(find extension -name 'hve-core-*.vsix' -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2) - echo "๐Ÿ“ฆ Publishing pre-release: $VSIX_FILE" + VSIX_FILE=$(find dist -name '*.vsix' -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2) + if [ -z "$VSIX_FILE" ]; then + echo "::error::No VSIX file found for collection ${{ matrix.id }}" + exit 1 + fi + VSIX_VERSION=$(basename "$VSIX_FILE" .vsix | grep -oE '[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$') + echo "version=$VSIX_VERSION" >> "$GITHUB_OUTPUT" + echo "๐Ÿ“ฆ Publishing pre-release ${{ matrix.id }}: $VSIX_FILE (v$VSIX_VERSION)" vsce publish --packagePath "$VSIX_FILE" --pre-release --azure-credential - name: Summary run: | { - echo "## ๐Ÿš€ Pre-Release Extension Published" + echo "## ๐Ÿš€ Pre-Release Extension Published: ${{ matrix.name }}" echo "" - echo "**Version:** ${{ needs.validate-version.outputs.version }}" + echo "**Collection:** ${{ matrix.id }}" + echo "**Version:** ${{ steps.publish.outputs.version }}" echo "**Channel:** Pre-Release (ODD minor)" - echo "**VSIX File:** ${{ needs.package.outputs.vsix-file }}" echo "" echo "Users can install via **Switch to Pre-Release Version** in VS Code." echo "" - echo "View on [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=ise-hve-essentials.hve-core)" + echo "View on [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=ise-hve-essentials.${{ matrix.name }})" } >> "$GITHUB_STEP_SUMMARY" diff --git a/docs/architecture/workflows.md b/docs/architecture/workflows.md index 56c123fc..34a3cd08 100644 --- a/docs/architecture/workflows.md +++ b/docs/architecture/workflows.md @@ -55,27 +55,28 @@ flowchart TD Individual validation workflows called by orchestration workflows: -| Workflow | Purpose | npm Script | -|-------------------------------|---------------------------------|----------------------------| -| `markdown-lint.yml` | Markdownlint validation | `npm run lint:md` | -| `spell-check.yml` | cspell dictionary check | `npm run spell-check` | -| `frontmatter-validation.yml` | AI artifact frontmatter schemas | `npm run lint:frontmatter` | -| `markdown-link-check.yml` | Broken link detection | `npm run lint:md-links` | -| `link-lang-check.yml` | Link language validation | `npm run lint:links` | -| `yaml-lint.yml` | YAML syntax validation | `npm run lint:yaml` | -| `ps-script-analyzer.yml` | PowerShell static analysis | `npm run lint:ps` | -| `table-format.yml` | Markdown table formatting | `npm run format:tables` | -| `pester-tests.yml` | PowerShell unit tests | `npm run test:ps` | -| `dependency-pinning-scan.yml` | GitHub Actions pinning | N/A (PowerShell direct) | -| `sha-staleness-check.yml` | SHA reference freshness | N/A (PowerShell direct) | -| `codeql-analysis.yml` | CodeQL security scanning | N/A (GitHub native) | -| `dependency-review.yml` | Dependency vulnerability review | N/A (GitHub native) | -| `security-scan.yml` | Composite security validation | N/A | -| `extension-package.yml` | VS Code extension packaging | N/A | +| Workflow | Purpose | npm Script | +|------------------------------------|---------------------------------|----------------------------| +| `markdown-lint.yml` | Markdownlint validation | `npm run lint:md` | +| `spell-check.yml` | cspell dictionary check | `npm run spell-check` | +| `frontmatter-validation.yml` | AI artifact frontmatter schemas | `npm run lint:frontmatter` | +| `markdown-link-check.yml` | Broken link detection | `npm run lint:md-links` | +| `link-lang-check.yml` | Link language validation | `npm run lint:links` | +| `yaml-lint.yml` | YAML syntax validation | `npm run lint:yaml` | +| `ps-script-analyzer.yml` | PowerShell static analysis | `npm run lint:ps` | +| `table-format.yml` | Markdown table formatting | `npm run format:tables` | +| `pester-tests.yml` | PowerShell unit tests | `npm run test:ps` | +| `dependency-pinning-scan.yml` | GitHub Actions pinning | N/A (PowerShell direct) | +| `sha-staleness-check.yml` | SHA reference freshness | N/A (PowerShell direct) | +| `codeql-analysis.yml` | CodeQL security scanning | N/A (GitHub native) | +| `dependency-review.yml` | Dependency vulnerability review | N/A (GitHub native) | +| `artifact-registry-validation.yml` | AI artifact registry validation | `npm run lint:registry` | +| `security-scan.yml` | Composite security validation | N/A | +| `extension-package.yml` | VS Code extension packaging | N/A | ## PR Validation Pipeline -The `pr-validation.yml` workflow serves as the primary quality gate for all pull requests. It runs 12 parallel jobs covering linting, security, and testing. +The `pr-validation.yml` workflow serves as the primary quality gate for all pull requests. It runs 13 parallel jobs covering linting, security, and testing. ```mermaid flowchart LR @@ -85,6 +86,7 @@ flowchart LR TF[table-format] YL[yaml-lint] FV[frontmatter-validation] + ARV[artifact-registry-validation] LLC[link-lang-check] MLC[markdown-link-check] end @@ -103,20 +105,21 @@ flowchart LR ### Jobs -| Job | Reusable Workflow | Validates | -|--------------------------|-------------------------------|--------------------------------| -| spell-check | `spell-check.yml` | Spelling across all files | -| markdown-lint | `markdown-lint.yml` | Markdown formatting rules | -| table-format | `table-format.yml` | Markdown table structure | -| psscriptanalyzer | `ps-script-analyzer.yml` | PowerShell code quality | -| yaml-lint | `yaml-lint.yml` | YAML syntax | -| pester-tests | `pester-tests.yml` | PowerShell unit tests | -| frontmatter-validation | `frontmatter-validation.yml` | AI artifact metadata | -| link-lang-check | `link-lang-check.yml` | Link accessibility | -| markdown-link-check | `markdown-link-check.yml` | Broken links | -| dependency-pinning-check | `dependency-pinning-scan.yml` | Action SHA pinning | -| npm-audit | Inline | npm dependency vulnerabilities | -| codeql | `codeql-analysis.yml` | Code security patterns | +| Job | Reusable Workflow | Validates | +|------------------------------|------------------------------------|--------------------------------| +| spell-check | `spell-check.yml` | Spelling across all files | +| markdown-lint | `markdown-lint.yml` | Markdown formatting rules | +| table-format | `table-format.yml` | Markdown table structure | +| psscriptanalyzer | `ps-script-analyzer.yml` | PowerShell code quality | +| yaml-lint | `yaml-lint.yml` | YAML syntax | +| pester-tests | `pester-tests.yml` | PowerShell unit tests | +| frontmatter-validation | `frontmatter-validation.yml` | AI artifact metadata | +| link-lang-check | `link-lang-check.yml` | Link accessibility | +| markdown-link-check | `markdown-link-check.yml` | Broken links | +| artifact-registry-validation | `artifact-registry-validation.yml` | AI artifact registry integrity | +| dependency-pinning-check | `dependency-pinning-scan.yml` | Action SHA pinning | +| npm-audit | Inline | npm dependency vulnerabilities | +| codeql | `codeql-analysis.yml` | Code security patterns | All jobs run in parallel with no dependencies, enabling fast feedback (typically under 3 minutes). @@ -174,23 +177,41 @@ The `weekly-security-maintenance.yml` workflow runs every Sunday at 2AM UTC, pro ## Extension Publishing -The `extension-publish.yml` workflow handles VS Code extension marketplace publishing through manual dispatch. +The `extension-publish.yml` and `extension-publish-prerelease.yml` workflows handle VS Code extension marketplace publishing through manual dispatch. Both workflows use collection-based packaging to produce and publish a separate VSIX per persona collection. ```mermaid flowchart LR - PC[prepare-changelog] --> PKG[package] - NV[normalize-version] --> PKG - PKG --> PUB[publish] + PC[prepare-changelog] --> DC[discover-collections] + NV[normalize-version] --> DC + DC --> PKG["package (matrix)"] + PKG --> PUB["publish (matrix)"] ``` ### Publishing Jobs -| Job | Purpose | -|-------------------|------------------------------------------| -| prepare-changelog | Extract release notes from CHANGELOG.md | -| normalize-version | Ensure version consistency | -| package | Build VSIX using `extension-package.yml` | -| publish | Upload to VS Code Marketplace via vsce | +| Job | Purpose | +|----------------------|-------------------------------------------------------------| +| prepare-changelog | Extract release notes from CHANGELOG.md | +| normalize-version | Ensure version consistency | +| validate-version | Enforce ODD minor version for pre-release channel | +| discover-collections | Scan collection manifests, filter by maturity and channel | +| package (matrix) | Build one VSIX per collection using `extension-package.yml` | +| publish (matrix) | Upload each VSIX to VS Code Marketplace via OIDC + vsce | + +### Collection-Based Packaging + +Collection manifests in `extension/collections/*.collection.json` define persona-scoped subsets of the full artifact set. The `extension-package.yml` reusable workflow discovers these manifests, filters by maturity and channel, and packages each as an independent VSIX. + +| Collection | Maturity | Included In | +|----------------|--------------|--------------------| +| `hve-core-all` | Stable | Stable, PreRelease | +| `developer` | Experimental | PreRelease only | + +Maturity filtering rules: + +* **Deprecated** collections are always excluded. +* **Experimental** collections are excluded from Stable channel builds. +* **Stable** collections are included in all channel builds. ### Version Channels @@ -203,17 +224,18 @@ flowchart LR Workflows invoke validation through npm scripts defined in `package.json`: -| npm Script | Command | Used By | -|--------------------|------------------------------------|----------------------------| -| `lint:md` | `markdownlint-cli2` | markdown-lint.yml | -| `spell-check` | `cspell` | spell-check.yml | -| `lint:frontmatter` | `Validate-MarkdownFrontmatter.ps1` | frontmatter-validation.yml | -| `lint:md-links` | `Markdown-Link-Check.ps1` | markdown-link-check.yml | -| `lint:links` | `Invoke-LinkLanguageCheck.ps1` | link-lang-check.yml | -| `lint:yaml` | `Invoke-YamlLint.ps1` | yaml-lint.yml | -| `lint:ps` | `Invoke-PSScriptAnalyzer.ps1` | ps-script-analyzer.yml | -| `format:tables` | `markdown-table-formatter` | table-format.yml | -| `test:ps` | `Invoke-Pester` | pester-tests.yml | +| npm Script | Command | Used By | +|--------------------|------------------------------------|----------------------------------| +| `lint:md` | `markdownlint-cli2` | markdown-lint.yml | +| `spell-check` | `cspell` | spell-check.yml | +| `lint:frontmatter` | `Validate-MarkdownFrontmatter.ps1` | frontmatter-validation.yml | +| `lint:md-links` | `Markdown-Link-Check.ps1` | markdown-link-check.yml | +| `lint:links` | `Invoke-LinkLanguageCheck.ps1` | link-lang-check.yml | +| `lint:yaml` | `Invoke-YamlLint.ps1` | yaml-lint.yml | +| `lint:ps` | `Invoke-PSScriptAnalyzer.ps1` | ps-script-analyzer.yml | +| `format:tables` | `markdown-table-formatter` | table-format.yml | +| `lint:registry` | `Validate-ArtifactRegistry.ps1` | artifact-registry-validation.yml | +| `test:ps` | `Invoke-Pester` | pester-tests.yml | ## Related Documentation From ad70b9cbc4d5fa6c5c64d60b1b19616b7775cb1a Mon Sep 17 00:00:00 2001 From: katriendg Date: Wed, 11 Feb 2026 10:01:50 +0100 Subject: [PATCH 40/62] feat(workflows): enhance version resolution logic in extension packaging workflow - implement effective version resolution based on input parameters - clean up pre-existing directories before packaging - add error handling for unexpected failures during orchestration --- .github/workflows/extension-package.yml | 13 ++++- .../extension/Package-Extension.Tests.ps1 | 47 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/.github/workflows/extension-package.yml b/.github/workflows/extension-package.yml index 56257ded..f99b2b89 100644 --- a/.github/workflows/extension-package.yml +++ b/.github/workflows/extension-package.yml @@ -49,10 +49,19 @@ jobs: with: persist-credentials: false - - name: Read package version + - name: Resolve effective version id: read-version shell: bash - run: echo "version=$(jq -r .version extension/package.json)" >> "$GITHUB_OUTPUT" + run: | + version="${{ inputs.version }}" + dev_patch="${{ inputs.dev-patch-number }}" + if [ -z "$version" ]; then + version=$(jq -r .version extension/package.json) + fi + if [ -n "$dev_patch" ]; then + version="${version}-dev.${dev_patch}" + fi + echo "version=$version" >> "$GITHUB_OUTPUT" - name: Discover collection manifests id: discover diff --git a/scripts/tests/extension/Package-Extension.Tests.ps1 b/scripts/tests/extension/Package-Extension.Tests.ps1 index 35c355bc..c5f4b8f3 100644 --- a/scripts/tests/extension/Package-Extension.Tests.ps1 +++ b/scripts/tests/extension/Package-Extension.Tests.ps1 @@ -584,6 +584,53 @@ Describe 'Invoke-PackageExtension' { $restored.description | Should -Be 'Original description' } } + + It 'Cleans pre-existing copied directories before preparing extension' { + Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } } + Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } } + + $manifest = @{ + name = 'test-ext' + version = '1.0.0' + publisher = 'test' + engines = @{ vscode = '^1.80.0' } + } + $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json') + + # Pre-create directories that should be cleaned before packaging + $preExistingGithub = Join-Path $script:extDir '.github/stale' + $preExistingScripts = Join-Path $script:extDir 'scripts/old' + New-Item -Path $preExistingGithub -ItemType Directory -Force | Out-Null + Set-Content -Path (Join-Path $preExistingGithub 'leftover.md') -Value 'stale' + New-Item -Path $preExistingScripts -ItemType Directory -Force | Out-Null + Set-Content -Path (Join-Path $preExistingScripts 'leftover.ps1') -Value 'stale' + + $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix' + Set-Content -Path $vsixPath -Value 'fake-vsix' + + $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot + + # Stale files should have been removed during pre-clean + $result | Should -BeOfType [hashtable] + } + + It 'Returns failure when an unexpected error occurs during orchestration' { + Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } } + Mock Get-PackagingDirectorySpec { throw 'Simulated unexpected failure' } + + $manifest = @{ + name = 'test-ext' + version = '1.0.0' + publisher = 'test' + engines = @{ vscode = '^1.80.0' } + } + $manifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json') + + $result = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot + + $result.Success | Should -BeFalse + $result.ErrorMessage | Should -Match 'Simulated unexpected failure' + } } Describe 'Test-PackagingInputsValid' { From 0e446c8ab9384aecf9ab3bb421d1cd59bdfbcb22 Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 17:36:13 -0800 Subject: [PATCH 41/62] feat(workflows): enhance plugin packaging and validation workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add new plugin-package.yml for packaging plugins - update main.yml to include plugin packaging and upload steps - improve plugin-validation.yml with metadata validation - remove artifact-registry-validation from PR validation workflow ๐Ÿ”ง - Generated by Copilot --- .github/agents/hve-core-installer.agent.md | 14 +-- .../artifact-registry-validation.yml | 21 +++- .github/workflows/extension-package.yml | 30 ++++-- .github/workflows/main.yml | 42 +++++++- .github/workflows/plugin-package.yml | 97 +++++++++++++++++++ .github/workflows/plugin-validation.yml | 94 +++++++++--------- .github/workflows/pr-validation.yml | 9 -- 7 files changed, 228 insertions(+), 79 deletions(-) create mode 100644 .github/workflows/plugin-package.yml diff --git a/.github/agents/hve-core-installer.agent.md b/.github/agents/hve-core-installer.agent.md index f9ba64e8..4c481a70 100644 --- a/.github/agents/hve-core-installer.agent.md +++ b/.github/agents/hve-core-installer.agent.md @@ -1095,9 +1095,9 @@ Read `.github/ai-artifacts-registry.json` from the HVE-Core source (at `$hveCore Choose one or more personas to install agents tailored to your role, more to come in the future. -| # | Persona | Agents | Description | -|---|--------------------|--------|----------------------------------| -| 1 | Developer | [N] | Software engineers writing code | +| # | Persona | Agents | Description | +|---|-----------|--------|---------------------------------| +| 1 | Developer | [N] | Software engineers writing code | Enter persona number(s) separated by commas (e.g., "1"): ``` @@ -1147,10 +1147,10 @@ User input handling: ### Agent Bundle Definitions -| Bundle | Agents | -| ------ | ------ | -| `rpi-core` | task-researcher, task-planner, task-implementor, task-reviewer, rpi-agent | -| `persona:` | Stable agents matching the persona | +| Bundle | Agents | +|----------------|---------------------------------------------------------------------------| +| `rpi-core` | task-researcher, task-planner, task-implementor, task-reviewer, rpi-agent | +| `persona:` | Stable agents matching the persona | ### Collision Detection diff --git a/.github/workflows/artifact-registry-validation.yml b/.github/workflows/artifact-registry-validation.yml index 2f6b7b9f..d3004e5e 100644 --- a/.github/workflows/artifact-registry-validation.yml +++ b/.github/workflows/artifact-registry-validation.yml @@ -1,15 +1,27 @@ -name: Artifact Registry Validation +name: Artifact Registry Validation (Fallback) on: + workflow_dispatch: + inputs: + soft-fail: + description: "Whether to continue on registry validation errors" + required: false + type: boolean + default: false + warnings-as-errors: + description: "Treat warnings as errors" + required: false + type: boolean + default: true workflow_call: inputs: soft-fail: - description: 'Whether to continue on registry validation errors' + description: "Whether to continue on registry validation errors" required: false type: boolean default: false warnings-as-errors: - description: 'Treat warnings as errors' + description: "Treat warnings as errors" required: false type: boolean default: true @@ -19,7 +31,7 @@ permissions: jobs: artifact-registry-validation: - name: Validate Artifact Registry + name: Validate Artifact Registry (Fallback) runs-on: ubuntu-latest permissions: contents: read @@ -38,6 +50,7 @@ jobs: id: validate shell: pwsh run: | + Write-Warning 'Artifact registry validation is deprecated and retained as fallback only.' $params = @{} if ('${{ inputs.warnings-as-errors }}' -eq 'true') { diff --git a/.github/workflows/extension-package.yml b/.github/workflows/extension-package.yml index f99b2b89..9b446724 100644 --- a/.github/workflows/extension-package.yml +++ b/.github/workflows/extension-package.yml @@ -4,31 +4,31 @@ on: workflow_call: inputs: version: - description: 'Full version to use (e.g., 1.0.0 or empty to use package.json)' + description: "Full version to use (e.g., 1.0.0 or empty to use package.json)" required: false type: string - default: '' + default: "" dev-patch-number: - description: 'Dev patch number to append (creates version like 1.0.0-dev.123)' + description: "Dev patch number to append (creates version like 1.0.0-dev.123)" required: false type: string - default: '' + default: "" use-changelog: - description: 'Whether to download and use changelog artifact' + description: "Whether to download and use changelog artifact" required: false type: boolean default: false channel: - description: 'Release channel (Stable or PreRelease) controlling agent maturity filtering' + description: "Release channel (Stable or PreRelease) controlling agent maturity filtering" required: false type: string - default: 'Stable' + default: "Stable" outputs: version: - description: 'Version that was packaged' + description: "Version that was packaged" value: ${{ jobs.discover-collections.outputs.version }} collections-matrix: - description: 'JSON matrix of collections that were actually packaged (filtered by channel and maturity)' + description: "JSON matrix of collections that were actually packaged (filtered by channel and maturity)" value: ${{ jobs.discover-collections.outputs.matrix }} permissions: @@ -49,6 +49,16 @@ jobs: with: persist-credentials: false + - name: Setup PowerShell modules + shell: pwsh + run: | + Install-Module -Name PowerShell-Yaml -Force -Scope CurrentUser + + - name: Generate extension collections and package templates + shell: pwsh + run: | + ./scripts/extension/Generate-ExtensionCollections.ps1 + - name: Resolve effective version id: read-version shell: bash @@ -121,7 +131,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.1.0 with: - node-version: '20' + node-version: "20" - name: Install dependencies run: npm install -g @vscode/vsce@3.7.1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7dfed33e..0d2ef58a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -107,6 +107,16 @@ jobs: permissions: contents: read + plugin-package-release: + name: Package Plugins (Release) + needs: [release-please] + if: ${{ needs.release-please.outputs.release_created == 'true' }} + uses: ./.github/workflows/plugin-package.yml + with: + version: ${{ needs.release-please.outputs.version }} + permissions: + contents: read + attest-and-upload: name: Attest and Upload (${{ matrix.id }}) needs: [release-please, extension-package-release] @@ -129,7 +139,7 @@ jobs: - name: Attest build provenance uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: - subject-path: 'dist/*.vsix' + subject-path: "dist/*.vsix" - name: Upload VSIX to GitHub Release env: @@ -142,9 +152,37 @@ jobs: fi gh release upload "${{ needs.release-please.outputs.tag_name }}" "$VSIX_FILE" --clobber -R "${{ github.repository }}" + upload-plugin-packages: + name: Upload Plugin Package (${{ matrix.id }}) + needs: [release-please, plugin-package-release] + if: ${{ needs.release-please.outputs.release_created == 'true' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.plugin-package-release.outputs.collections-matrix) }} + permissions: + contents: write + steps: + - name: Download plugin artifact + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: plugin-package-${{ matrix.id }} + path: ./dist/plugins + + - name: Upload plugin package to GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + PLUGIN_ARCHIVE="dist/plugins/${{ matrix.id }}.zip" + if [ ! -f "$PLUGIN_ARCHIVE" ]; then + echo "::error::Plugin archive not found for collection ${{ matrix.id }}" + exit 1 + fi + gh release upload "${{ needs.release-please.outputs.tag_name }}" "$PLUGIN_ARCHIVE" --clobber -R "${{ github.repository }}" + publish-release: name: Publish GitHub Release - needs: [release-please, attest-and-upload] + needs: [release-please, attest-and-upload, upload-plugin-packages] if: ${{ needs.release-please.outputs.release_created == 'true' }} runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/plugin-package.yml b/.github/workflows/plugin-package.yml new file mode 100644 index 00000000..ab5d23ba --- /dev/null +++ b/.github/workflows/plugin-package.yml @@ -0,0 +1,97 @@ +name: Package Plugins + +on: + workflow_call: + inputs: + version: + description: "Release version (for artifact metadata)" + required: false + type: string + default: "" + outputs: + collections-matrix: + description: "JSON matrix of plugin collections that were packaged" + value: ${{ jobs.discover-collections.outputs.matrix }} + +permissions: + contents: read + +jobs: + discover-collections: + name: Discover Plugin Collections + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + matrix: ${{ steps.discover.outputs.matrix }} + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 + with: + persist-credentials: false + + - name: Discover collection manifests + id: discover + shell: bash + run: | + matrix_json=$(find collections -name '*.collection.yml' -type f | sort | while IFS= read -r file; do + id=$(basename "$file" .collection.yml) + echo "{\"id\":\"$id\"}" + done | jq -s '{include: .}') + + echo "matrix=$matrix_json" >> "$GITHUB_OUTPUT" + echo "Discovered plugin collections:" + echo "$matrix_json" | jq . + + package: + name: Package Plugin ${{ matrix.id }} + needs: discover-collections + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.discover-collections.outputs.matrix) }} + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.1.0 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Install PowerShell-Yaml + shell: pwsh + run: | + if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) { + Install-Module -Name PowerShell-Yaml -Force -Scope CurrentUser + } + + - name: Generate plugins + run: npm run plugin:generate + + - name: Package plugin directory + shell: bash + run: | + plugin_dir="plugins/${{ matrix.id }}" + if [ ! -d "$plugin_dir" ]; then + echo "::error::Expected plugin directory not found: $plugin_dir" + exit 1 + fi + + mkdir -p dist/plugins + zip -r "dist/plugins/${{ matrix.id }}.zip" "$plugin_dir" + + - name: Upload plugin artifact + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4.4.3 + with: + name: plugin-package-${{ matrix.id }} + path: dist/plugins/${{ matrix.id }}.zip + retention-days: 30 diff --git a/.github/workflows/plugin-validation.yml b/.github/workflows/plugin-validation.yml index a20335bd..f01b572a 100644 --- a/.github/workflows/plugin-validation.yml +++ b/.github/workflows/plugin-validation.yml @@ -1,54 +1,54 @@ name: Plugin Validation on: - workflow_call: - inputs: - soft-fail: - description: "Whether to continue on validation errors" - required: false - type: boolean - default: false + workflow_call: + inputs: + soft-fail: + description: "Whether to continue on validation errors" + required: false + type: boolean + default: false permissions: - contents: read + contents: read jobs: - validate: - name: Validate Plugins - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 - with: - persist-credentials: false - - - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.1.0 - with: - node-version: "20" - cache: "npm" - - - name: Install dependencies - run: npm ci - - - name: Install PowerShell-Yaml - shell: pwsh - run: | - if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) { - Install-Module -Name PowerShell-Yaml -Force -Scope CurrentUser - } - - - name: Validate collections - run: npm run plugin:validate - - - name: Check plugin freshness - run: | - npm run plugin:generate - if ! git diff --quiet plugins/; then - echo "::error::Plugins out of date. Run 'npm run plugin:generate' and commit." - if [ "${{ inputs.soft-fail }}" != "true" ]; then - exit 1 - fi - fi + validate: + name: Validate Plugins + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4.1.0 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Install PowerShell-Yaml + shell: pwsh + run: | + if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) { + Install-Module -Name PowerShell-Yaml -Force -Scope CurrentUser + } + + - name: Validate collection metadata + run: npm run lint:collections-metadata + + - name: Check plugin freshness + run: | + npm run plugin:generate + if ! git diff --quiet plugins/; then + echo "::error::Plugins out of date. Run 'npm run plugin:generate' and commit." + if [ "${{ inputs.soft-fail }}" != "true" ]; then + exit 1 + fi + fi diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 88ce8d9d..7b03a907 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -89,15 +89,6 @@ jobs: skip-footer-validation: false warnings-as-errors: true - artifact-registry-validation: - name: Artifact Registry Validation - uses: ./.github/workflows/artifact-registry-validation.yml - permissions: - contents: read - with: - soft-fail: false - warnings-as-errors: true - plugin-validation: name: Plugin Validation uses: ./.github/workflows/plugin-validation.yml From 3d9b65d831e0f05eb06d3200b733e90521af13a0 Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 17:36:26 -0800 Subject: [PATCH 42/62] feat(extension): add dry-run parameter for packaging orchestration validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - implement dry-run functionality to validate packaging without creating VSIX - enhance documentation for new parameter usage feat(scripts): introduce maturity handling for collection artifacts - add maturity resolution logic for collection items - implement filtering of collection items based on maturity levels - update validation to check for maturity conflicts across collections feat(plugins): add channel parameter to control item maturity eligibility - implement filtering of collection items based on release channel - enhance plugin generation to respect maturity settings feat(schemas): extend collection manifest schema to include maturity field - define maturity options in JSON schema for collection items ๐Ÿ”’ - Generated by Copilot --- collections/hve-core-all.collection.yml | 2 + docs/contributing/ai-artifacts-common.md | 17 +- extension/templates/collection.template.json | 8 + extension/templates/package.template.json | 24 ++ package.json | 8 +- plugins/hve-core-all/README.md | 1 + .../agents/github-issue-manager.md | 1 + .../Generate-ExtensionCollections.ps1 | 331 ++++++++++++++++++ scripts/extension/Package-Extension.ps1 | 25 +- scripts/extension/Prepare-Extension.ps1 | 230 ++++++++---- .../schemas/collection-manifest.schema.json | 5 + scripts/plugins/Generate-Plugins.ps1 | 104 +++++- scripts/plugins/Modules/PluginHelpers.psm1 | 159 +++++---- scripts/plugins/Validate-Collections.ps1 | 114 ++++++ 14 files changed, 876 insertions(+), 153 deletions(-) create mode 100644 extension/templates/collection.template.json create mode 100644 extension/templates/package.template.json create mode 120000 plugins/hve-core-all/agents/github-issue-manager.md create mode 100644 scripts/extension/Generate-ExtensionCollections.ps1 diff --git a/collections/hve-core-all.collection.yml b/collections/hve-core-all.collection.yml index 97f33db9..cfbba59f 100644 --- a/collections/hve-core-all.collection.yml +++ b/collections/hve-core-all.collection.yml @@ -24,6 +24,8 @@ items: kind: agent - path: .github/agents/github-backlog-manager.agent.md kind: agent +- path: .github/agents/github-issue-manager.agent.md + kind: agent - path: .github/agents/hve-core-installer.agent.md kind: agent - path: .github/agents/memory.agent.md diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 89e2d768..ebc9e47d 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -269,7 +269,7 @@ For clone-based installations, the installer agent supports **agent-only persona ## Maturity Field Requirements -Maturity is defined in `.github/ai-artifacts-registry.json` and MUST NOT appear in artifact frontmatter. +Maturity is defined in `collections/*.collection.yml` under `items[].maturity` and MUST NOT appear in artifact frontmatter. ### Purpose @@ -287,6 +287,8 @@ The maturity field controls which extension channel includes the artifact: | `experimental` | Early development, may change significantly | โŒ Excluded | โœ… Included | | `deprecated` | Scheduled for removal | โŒ Excluded | โŒ Excluded | +When `items[].maturity` is omitted, the effective maturity defaults to `stable`. + ### Default for New Contributions New artifact registry entries **SHOULD** use `maturity: stable` unless: @@ -297,14 +299,13 @@ New artifact registry entries **SHOULD** use `maturity: stable` unless: ### Setting Maturity -Add or update the maturity value in the artifact's registry entry in `.github/ai-artifacts-registry.json`: +Add or update the maturity value on each collection item in `collections/*.collection.yml`: -```json -"my-artifact": { - "maturity": "stable", - "personas": ["hve-core-all"], - "tags": ["category"] -} +```yaml +items: + - path: .github/agents/example.agent.md + kind: agent + maturity: stable ``` For detailed channel and lifecycle information, see [Release Process - Extension Channels](release-process.md#extension-channels-and-maturity). diff --git a/extension/templates/collection.template.json b/extension/templates/collection.template.json new file mode 100644 index 00000000..f4ff0981 --- /dev/null +++ b/extension/templates/collection.template.json @@ -0,0 +1,8 @@ +{ + "$schema": "../../scripts/linting/schemas/collection.schema.json", + "id": "", + "name": "", + "displayName": "", + "description": "", + "maturity": "stable" +} diff --git a/extension/templates/package.template.json b/extension/templates/package.template.json new file mode 100644 index 00000000..d184f3c5 --- /dev/null +++ b/extension/templates/package.template.json @@ -0,0 +1,24 @@ +{ + "name": "hve-core", + "displayName": "HVE Core", + "extensionKind": [ + "workspace", + "ui" + ], + "version": "2.2.0", + "description": "AI-powered chat agents, prompts, and instructions for hybrid virtual environments", + "publisher": "ise-hve-essentials", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/hve-core.git" + }, + "engines": { + "vscode": "^1.106.1" + }, + "categories": [ + "Chat" + ], + "contributes": {}, + "author": "Microsoft", + "license": "MIT" +} diff --git a/package.json b/package.json index 7e70473f..dd94081d 100644 --- a/package.json +++ b/package.json @@ -14,17 +14,19 @@ "lint:links": "pwsh -NoProfile -Command \"& scripts/linting/Invoke-LinkLanguageCheck.ps1 -ExcludePaths 'scripts/tests/**'\"", "lint:md-links": "pwsh -File scripts/linting/Markdown-Link-Check.ps1", "lint:frontmatter": "pwsh -NoProfile -Command \"& './scripts/linting/Validate-MarkdownFrontmatter.ps1' -WarningsAsErrors -EnableSchemaValidation\"", + "lint:collections-metadata": "pwsh -File scripts/plugins/Validate-Collections.ps1", "lint:registry": "pwsh -NoProfile -Command \"& './scripts/linting/Validate-ArtifactRegistry.ps1' -WarningsAsErrors\"", "lint:version-consistency": "pwsh -NoProfile -Command \"./scripts/security/Test-ActionVersionConsistency.ps1 -FailOnMismatch\"", - "lint:all": "npm run format:tables && npm run lint:md && npm run lint:ps && npm run lint:yaml && npm run lint:links && npm run lint:frontmatter && npm run lint:registry && npm run lint:version-consistency", + "lint:all": "npm run format:tables && npm run lint:md && npm run lint:ps && npm run lint:yaml && npm run lint:links && npm run lint:frontmatter && npm run lint:collections-metadata && npm run lint:version-consistency", "format:tables": "markdown-table-formatter \"**/*.md\"", "extension:prepare": "pwsh ./scripts/extension/Prepare-Extension.ps1", "extension:prepare:prerelease": "pwsh ./scripts/extension/Prepare-Extension.ps1 -Channel PreRelease", "extension:package": "pwsh ./scripts/extension/Package-Extension.ps1", + "package:extension": "npm run extension:package --", "extension:package:prerelease": "pwsh ./scripts/extension/Package-Extension.ps1 -PreRelease", "validate:copyright": "pwsh -File scripts/linting/Test-CopyrightHeaders.ps1", "plugin:generate": "pwsh -File scripts/plugins/Generate-Plugins.ps1 && npm run lint:md:fix && npm run format:tables", - "plugin:validate": "pwsh -File scripts/plugins/Validate-Collections.ps1" + "plugin:validate": "npm run lint:collections-metadata" }, "devDependencies": { "cspell": "9.6.4", @@ -41,4 +43,4 @@ }, "author": "Microsoft", "license": "MIT" -} \ No newline at end of file +} diff --git a/plugins/hve-core-all/README.md b/plugins/hve-core-all/README.md index 6b6c6b53..56ab1cf3 100644 --- a/plugins/hve-core-all/README.md +++ b/plugins/hve-core-all/README.md @@ -22,6 +22,7 @@ copilot plugin install hve-core-all@hve-core | gen-jupyter-notebook | Create structured exploratory data analysis Jupyter notebooks from available data sources and generated data dictionaries | | gen-streamlit-dashboard | Develop a multi-page Streamlit dashboard | | github-backlog-manager | Orchestrator agent for GitHub backlog management workflows including triage, discovery, sprint planning, and execution - Brought to you by microsoft/hve-core | +| github-issue-manager | Deprecated: replaced by github-backlog-manager.agent.md for GitHub issue and backlog management | | hve-core-installer | Decision-driven installer for HVE-Core with 6 installation methods for local, devcontainer, and Codespaces environments - Brought to you by microsoft/hve-core | | memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | | pr-review | Comprehensive Pull Request review assistant ensuring code quality, security, and convention compliance - Brought to you by microsoft/hve-core | diff --git a/plugins/hve-core-all/agents/github-issue-manager.md b/plugins/hve-core-all/agents/github-issue-manager.md new file mode 120000 index 00000000..9d876439 --- /dev/null +++ b/plugins/hve-core-all/agents/github-issue-manager.md @@ -0,0 +1 @@ +../../../.github/agents/github-issue-manager.agent.md \ No newline at end of file diff --git a/scripts/extension/Generate-ExtensionCollections.ps1 b/scripts/extension/Generate-ExtensionCollections.ps1 new file mode 100644 index 00000000..a3e0ff16 --- /dev/null +++ b/scripts/extension/Generate-ExtensionCollections.ps1 @@ -0,0 +1,331 @@ +#!/usr/bin/env pwsh +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: MIT +#Requires -Version 7.0 + +<# +.SYNOPSIS + Generates extension collection manifests and package templates from root collections. + +.DESCRIPTION + Reads root collection manifests from collections/*.collection.yml and generates: + - extension/collections/*.collection.json + - extension/package.{collection-id}.json (plus canonical extension/package.json for hve-core-all) + + Generated output is deterministic for identical input files. + +.PARAMETER ValidateDeterminism + Runs generation twice in temporary directories and fails when outputs differ. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [switch]$ValidateDeterminism +) + +$ErrorActionPreference = 'Stop' + +Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force + +function Get-CollectionMaturity { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$CollectionManifest + ) + + if ($CollectionManifest.ContainsKey('maturity') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionManifest.maturity)) { + return [string]$CollectionManifest.maturity + } + + return 'stable' +} + +function Get-CollectionDisplayName { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$CollectionManifest, + + [Parameter(Mandatory = $true)] + [string]$DefaultValue + ) + + if ($CollectionManifest.ContainsKey('displayName') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionManifest.displayName)) { + return [string]$CollectionManifest.displayName + } + + if ($CollectionManifest.ContainsKey('name') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionManifest.name)) { + return "HVE Core - $($CollectionManifest.name)" + } + + return $DefaultValue +} + +function Copy-TemplateWithOverrides { + [CmdletBinding()] + [OutputType([pscustomobject])] + param( + [Parameter(Mandatory = $true)] + [pscustomobject]$Template, + + [Parameter(Mandatory = $true)] + [hashtable]$Overrides + ) + + $output = [ordered]@{} + + foreach ($propertyName in $Template.PSObject.Properties.Name) { + if ($Overrides.ContainsKey($propertyName)) { + $output[$propertyName] = $Overrides[$propertyName] + } + else { + $output[$propertyName] = $Template.$propertyName + } + } + + foreach ($propertyName in $Overrides.Keys | Sort-Object) { + if (-not $output.Contains($propertyName)) { + $output[$propertyName] = $Overrides[$propertyName] + } + } + + return [pscustomobject]$output +} + +function Set-JsonFile { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter(Mandatory = $true)] + [object]$Content + ) + + $parent = Split-Path -Path $Path -Parent + if (-not (Test-Path -Path $parent)) { + New-Item -Path $parent -ItemType Directory -Force | Out-Null + } + + $json = $Content | ConvertTo-Json -Depth 30 + Set-Content -Path $Path -Value $json -Encoding utf8NoBOM +} + +function Remove-StaleGeneratedFiles { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$RepoRoot, + + [Parameter(Mandatory = $true)] + [string[]]$ExpectedFiles + ) + + $expected = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($file in $ExpectedFiles) { + $null = $expected.Add([System.IO.Path]::GetFullPath($file)) + } + + $collectionsDir = Join-Path $RepoRoot 'extension/collections' + if (Test-Path $collectionsDir) { + Get-ChildItem -Path $collectionsDir -Filter '*.collection.json' -File | ForEach-Object { + $fullPath = [System.IO.Path]::GetFullPath($_.FullName) + if (-not $expected.Contains($fullPath)) { + Remove-Item -Path $_.FullName -Force + } + } + } + + $extensionDir = Join-Path $RepoRoot 'extension' + Get-ChildItem -Path $extensionDir -Filter 'package.*.json' -File | ForEach-Object { + $fullPath = [System.IO.Path]::GetFullPath($_.FullName) + if (-not $expected.Contains($fullPath)) { + Remove-Item -Path $_.FullName -Force + } + } +} + +function Get-GenerationHashes { + [CmdletBinding()] + [OutputType([string[]])] + param( + [Parameter(Mandatory = $true)] + [string]$OutputRoot + ) + + $patterns = @( + (Join-Path $OutputRoot 'extension/collections/*.collection.json'), + (Join-Path $OutputRoot 'extension/package.json'), + (Join-Path $OutputRoot 'extension/package.*.json') + ) + + $files = @() + foreach ($pattern in $patterns) { + $files += @(Get-ChildItem -Path $pattern -File -ErrorAction SilentlyContinue) + } + + $normalizedOutputRoot = [System.IO.Path]::GetFullPath($OutputRoot) + + $hashes = @( + $files | + Sort-Object FullName | + ForEach-Object { + $relativePath = [System.IO.Path]::GetRelativePath($normalizedOutputRoot, $_.FullName) -replace '\\', '/' + $hash = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash + "$relativePath::$hash" + } + ) + + return $hashes +} + +function Invoke-ExtensionCollectionsGeneration { + [CmdletBinding()] + [OutputType([string[]])] + param( + [Parameter(Mandatory = $true)] + [string]$RepoRoot + ) + + $collectionsDir = Join-Path $RepoRoot 'collections' + $extensionCollectionsDir = Join-Path $RepoRoot 'extension/collections' + $templatesDir = Join-Path $RepoRoot 'extension/templates' + + $collectionTemplatePath = Join-Path $templatesDir 'collection.template.json' + $packageTemplatePath = Join-Path $templatesDir 'package.template.json' + + if (-not (Test-Path $collectionTemplatePath)) { + throw "Collection template not found: $collectionTemplatePath" + } + if (-not (Test-Path $packageTemplatePath)) { + throw "Package template not found: $packageTemplatePath" + } + + if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) { + throw "Required module 'PowerShell-Yaml' is not installed." + } + + Import-Module PowerShell-Yaml -ErrorAction Stop + + $collectionTemplate = Get-Content -Path $collectionTemplatePath -Raw | ConvertFrom-Json + $packageTemplate = Get-Content -Path $packageTemplatePath -Raw | ConvertFrom-Json + + $collectionFiles = Get-ChildItem -Path $collectionsDir -Filter '*.collection.yml' -File | Sort-Object Name + if ($collectionFiles.Count -eq 0) { + throw "No root collection files found in $collectionsDir" + } + + New-Item -Path $extensionCollectionsDir -ItemType Directory -Force | Out-Null + + $expectedFiles = @() + + foreach ($collectionFile in $collectionFiles) { + $collection = ConvertFrom-Yaml -Yaml (Get-Content -Path $collectionFile.FullName -Raw) + if ($collection -isnot [hashtable]) { + throw "Collection manifest must be a hashtable: $($collectionFile.FullName)" + } + + $collectionId = [string]$collection.id + if ([string]::IsNullOrWhiteSpace($collectionId)) { + throw "Collection id is required: $($collectionFile.FullName)" + } + + $collectionDescription = if ($collection.ContainsKey('description')) { [string]$collection.description } else { [string]$packageTemplate.description } + $collectionMaturity = Get-CollectionMaturity -CollectionManifest $collection + + $extensionName = if ($collectionId -eq 'hve-core-all') { [string]$packageTemplate.name } else { "hve-$collectionId" } + $extensionDisplayName = if ($collectionId -eq 'hve-core-all') { + [string]$packageTemplate.displayName + } + else { + Get-CollectionDisplayName -CollectionManifest $collection -DefaultValue ([string]$packageTemplate.displayName) + } + + $collectionManifest = Copy-TemplateWithOverrides -Template $collectionTemplate -Overrides @{ + id = $collectionId + name = $extensionName + displayName = $extensionDisplayName + description = $collectionDescription + maturity = $collectionMaturity + } + + $collectionManifestPath = Join-Path $extensionCollectionsDir "$collectionId.collection.json" + Set-JsonFile -Path $collectionManifestPath -Content $collectionManifest + $expectedFiles += $collectionManifestPath + + $packageTemplateOutput = Copy-TemplateWithOverrides -Template $packageTemplate -Overrides @{ + name = $extensionName + displayName = $extensionDisplayName + description = $collectionDescription + } + + $packagePath = if ($collectionId -eq 'hve-core-all') { + Join-Path $RepoRoot 'extension/package.json' + } + else { + Join-Path $RepoRoot "extension/package.$collectionId.json" + } + + Set-JsonFile -Path $packagePath -Content $packageTemplateOutput + $expectedFiles += $packagePath + } + + Remove-StaleGeneratedFiles -RepoRoot $RepoRoot -ExpectedFiles $expectedFiles + + return $expectedFiles +} + +if ($MyInvocation.InvocationName -ne '.') { + try { + $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + $repoRoot = (Get-Item "$scriptDir/../..").FullName + + $generated = Invoke-ExtensionCollectionsGeneration -RepoRoot $repoRoot + + if ($ValidateDeterminism) { + $tempA = Join-Path ([System.IO.Path]::GetTempPath()) ("hve-extgen-A-" + [System.Guid]::NewGuid().ToString('N')) + $tempB = Join-Path ([System.IO.Path]::GetTempPath()) ("hve-extgen-B-" + [System.Guid]::NewGuid().ToString('N')) + + try { + New-Item -Path $tempA -ItemType Directory -Force | Out-Null + New-Item -Path $tempB -ItemType Directory -Force | Out-Null + New-Item -Path (Join-Path $tempA 'extension') -ItemType Directory -Force | Out-Null + New-Item -Path (Join-Path $tempB 'extension') -ItemType Directory -Force | Out-Null + + Copy-Item -Path (Join-Path $repoRoot 'collections') -Destination $tempA -Recurse + Copy-Item -Path (Join-Path $repoRoot 'collections') -Destination $tempB -Recurse + Copy-Item -Path (Join-Path $repoRoot 'extension/templates') -Destination (Join-Path $tempA 'extension/templates') -Recurse + Copy-Item -Path (Join-Path $repoRoot 'extension/templates') -Destination (Join-Path $tempB 'extension/templates') -Recurse + + Invoke-ExtensionCollectionsGeneration -RepoRoot $tempA | Out-Null + Invoke-ExtensionCollectionsGeneration -RepoRoot $tempB | Out-Null + + $hashesA = Get-GenerationHashes -OutputRoot $tempA + $hashesB = Get-GenerationHashes -OutputRoot $tempB + + $contentA = ($hashesA -join "`n") + $contentB = ($hashesB -join "`n") + if ($contentA -ne $contentB) { + throw 'Determinism validation failed: generated outputs differ for identical inputs.' + } + } + finally { + if (Test-Path $tempA) { Remove-Item -Path $tempA -Recurse -Force } + if (Test-Path $tempB) { Remove-Item -Path $tempB -Recurse -Force } + } + } + + Write-Host "Generated $($generated.Count) extension artifacts." -ForegroundColor Green + Set-CIOutput -Name 'generated-files-count' -Value $generated.Count + exit 0 + } + catch { + Write-Error -ErrorAction Continue "Generate-ExtensionCollections failed: $($_.Exception.Message)" + Write-CIAnnotation -Message $_.Exception.Message -Level Error + exit 1 + } +} diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index d7fd7ccb..1944b376 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -32,6 +32,9 @@ collection-filtered artifacts are copied and the output filename uses the collection ID. +.PARAMETER DryRun + Optional. Validates packaging orchestration without invoking vsce. + .EXAMPLE ./Package-Extension.ps1 # Packages using version from package.json @@ -76,7 +79,11 @@ param( [switch]$PreRelease, [Parameter(Mandatory = $false)] - [string]$Collection = "" + [string]$Collection = "", + + [Parameter(Mandatory = $false)] + [Alias('dry-run')] + [switch]$DryRun ) $ErrorActionPreference = 'Stop' @@ -759,6 +766,8 @@ function Invoke-PackageExtension { Optional path to a collection manifest JSON file. When specified, only collection-filtered artifacts are copied and the output filename uses the collection ID. + .PARAMETER DryRun + When specified, validates packaging orchestration without invoking vsce. .OUTPUTS Hashtable with Success, OutputPath, Version, and ErrorMessage properties. #> @@ -786,7 +795,10 @@ function Invoke-PackageExtension { [switch]$PreRelease, [Parameter(Mandatory = $false)] - [string]$Collection = "" + [string]$Collection = "", + + [Parameter(Mandatory = $false)] + [switch]$DryRun ) $dirsToClean = @(".github", "docs", "scripts") @@ -938,6 +950,12 @@ function Invoke-PackageExtension { } } + if ($DryRun) { + Write-Host "" + Write-Host "๐Ÿงช Dry-run complete: packaging orchestration validated without VSIX creation." -ForegroundColor Yellow + return New-PackagingResult -Success $true -Version $packageVersion + } + # Check vsce availability using pure function $vsceAvailability = Test-VsceAvailable if (-not $vsceAvailability.IsAvailable) { @@ -1041,7 +1059,8 @@ if ($MyInvocation.InvocationName -ne '.') { -DevPatchNumber $DevPatchNumber ` -ChangelogPath $ChangelogPath ` -PreRelease:$PreRelease ` - -Collection $Collection + -Collection $Collection ` + -DryRun:$DryRun if (-not $result.Success) { Write-Error -ErrorAction Continue $result.ErrorMessage diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index f28ded75..74e71b42 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -241,34 +241,108 @@ function Test-GlobMatch { return $false } +function Get-CollectionArtifactKey { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [string]$Kind, + + [Parameter(Mandatory = $true)] + [string]$Path + ) + + switch ($Kind) { + 'agent' { + return ([System.IO.Path]::GetFileName($Path) -replace '\.agent\.md$', '') + } + 'prompt' { + return ([System.IO.Path]::GetFileName($Path) -replace '\.prompt\.md$', '') + } + 'instruction' { + return ($Path -replace '^\.github/instructions/', '' -replace '\.instructions\.md$', '') + } + 'skill' { + return [System.IO.Path]::GetFileName($Path.TrimEnd('/')) + } + default { + if ($Path -match "\.$([regex]::Escape($Kind))\.md$") { + return ([System.IO.Path]::GetFileName($Path) -replace "\.$([regex]::Escape($Kind))\.md$", '') + } + + if ($Path -like '*.md') { + return [System.IO.Path]::GetFileNameWithoutExtension($Path) + } + + return [System.IO.Path]::GetFileName($Path) + } + } +} + +function Get-CollectionArtifactMaturity { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$CollectionItem, + + [Parameter(Mandatory = $false)] + [hashtable]$Registry = @{} + ) + + if ($CollectionItem.ContainsKey('maturity') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionItem.maturity)) { + return [string]$CollectionItem.maturity + } + + if (-not $CollectionItem.ContainsKey('kind') -or -not $CollectionItem.ContainsKey('path')) { + return 'stable' + } + + $kind = [string]$CollectionItem.kind + $path = [string]$CollectionItem.path + if ([string]::IsNullOrWhiteSpace($kind) -or [string]::IsNullOrWhiteSpace($path)) { + return 'stable' + } + + $registryType = "${kind}s" + if ($Registry.Count -gt 0 -and $Registry.ContainsKey($registryType)) { + $artifactKey = Get-CollectionArtifactKey -Kind $kind -Path $path + if ($Registry[$registryType].ContainsKey($artifactKey) -and $Registry[$registryType][$artifactKey].ContainsKey('maturity')) { + return [string]$Registry[$registryType][$artifactKey].maturity + } + } + + return 'stable' +} + function Get-CollectionArtifacts { <# .SYNOPSIS - Filters registry artifacts by collection persona, maturity, and glob patterns. + Filters collection artifacts by collection item metadata and channel maturity. .DESCRIPTION - Applies collection-level filtering to the artifact registry, returning artifact - names that match the collection's persona requirements, allowed maturities, - and optional include/exclude glob patterns. - .PARAMETER Registry - AI artifacts registry hashtable. + Applies collection-level filtering to manifest items, returning artifact + names that match allowed maturities. Item-level maturity is preferred; + registry maturity is used as temporary fallback when item maturity is omitted. .PARAMETER Collection - Collection manifest hashtable with personas and optional include/exclude. + Collection manifest hashtable with items. .PARAMETER AllowedMaturities Array of maturity levels to include. + .PARAMETER Registry + AI artifacts registry hashtable used as fallback for omitted item maturity. .OUTPUTS [hashtable] With Agents, Prompts, Instructions, Skills arrays of matching artifact names. #> [CmdletBinding()] [OutputType([hashtable])] param( - [Parameter(Mandatory = $true)] - [hashtable]$Registry, - [Parameter(Mandatory = $true)] [hashtable]$Collection, [Parameter(Mandatory = $true)] - [string[]]$AllowedMaturities + [string[]]$AllowedMaturities, + + [Parameter(Mandatory = $false)] + [hashtable]$Registry = @{} ) $result = @{ @@ -278,53 +352,29 @@ function Get-CollectionArtifacts { Skills = @() } - $collectionPersonas = $Collection.personas - - foreach ($type in @('agents', 'prompts', 'instructions', 'skills')) { - if (-not $Registry.ContainsKey($type)) { continue } + if (-not $Collection.ContainsKey('items') -or @($Collection.items).Count -eq 0) { + return $result + } - $includePatterns = @() - $excludePatterns = @() - if ($Collection.ContainsKey('include') -and $Collection.include.ContainsKey($type)) { - $includePatterns = $Collection.include[$type] - } - if ($Collection.ContainsKey('exclude') -and $Collection.exclude.ContainsKey($type)) { - $excludePatterns = $Collection.exclude[$type] + foreach ($item in $Collection.items) { + if (-not $item.ContainsKey('kind') -or -not $item.ContainsKey('path')) { + continue } - foreach ($name in $Registry[$type].Keys) { - $entry = $Registry[$type][$name] - - # Persona filter: artifact must belong to at least one collection persona - # Empty personas array means universal (all personas) - $personaMatch = $false - if (@($entry.personas).Count -eq 0) { - $personaMatch = $true - } else { - foreach ($persona in $entry.personas) { - if ($collectionPersonas -contains $persona) { - $personaMatch = $true - break - } - } - } - if (-not $personaMatch) { continue } - - # Maturity filter - if ($AllowedMaturities -notcontains $entry.maturity) { continue } + $kind = [string]$item.kind + $path = [string]$item.path - # Include glob filter (if specified) - if ($includePatterns.Count -gt 0 -and -not (Test-GlobMatch -Name $name -Patterns $includePatterns)) { - continue - } - - # Exclude glob filter - if ($excludePatterns.Count -gt 0 -and (Test-GlobMatch -Name $name -Patterns $excludePatterns)) { - continue - } + $maturity = Get-CollectionArtifactMaturity -CollectionItem $item -Registry $Registry + if ($AllowedMaturities -notcontains $maturity) { + continue + } - $capitalType = @{ agents = 'Agents'; prompts = 'Prompts'; instructions = 'Instructions'; skills = 'Skills' }[$type] - $result[$capitalType] += $name + $artifactKey = Get-CollectionArtifactKey -Kind $kind -Path $path + switch ($kind) { + 'agent' { $result.Agents += $artifactKey } + 'prompt' { $result.Prompts += $artifactKey } + 'instruction' { $result.Instructions += $artifactKey } + 'skill' { $result.Skills += $artifactKey } } } @@ -438,6 +488,9 @@ function Resolve-RequiresDependencies { AI artifacts registry hashtable. .PARAMETER AllowedMaturities Array of maturity levels to include. + .PARAMETER CollectionMaturities + Optional per-type maturity map keyed by artifact name. Used as + primary maturity source before registry lookup. .OUTPUTS [hashtable] With Agents, Prompts, Instructions, Skills arrays containing resolved names. #> @@ -451,7 +504,10 @@ function Resolve-RequiresDependencies { [hashtable]$Registry, [Parameter(Mandatory = $true)] - [string[]]$AllowedMaturities + [string[]]$AllowedMaturities, + + [Parameter(Mandatory = $false)] + [hashtable]$CollectionMaturities = @{} ) $resolved = @{ @@ -502,12 +558,17 @@ function Resolve-RequiresDependencies { $capitalType = $typeMap[$type] foreach ($dep in $requires[$type]) { - # Check maturity of dependency - if ($Registry.ContainsKey($type) -and $Registry[$type].ContainsKey($dep)) { + # Check maturity of dependency (collection metadata preferred, registry fallback) + $depMaturity = 'stable' + if ($CollectionMaturities.ContainsKey($type) -and $CollectionMaturities[$type].ContainsKey($dep)) { + $depMaturity = $CollectionMaturities[$type][$dep] + } + elseif ($Registry.ContainsKey($type) -and $Registry[$type].ContainsKey($dep) -and $Registry[$type][$dep].ContainsKey('maturity')) { $depMaturity = $Registry[$type][$dep].maturity - if ($AllowedMaturities -notcontains $depMaturity) { continue } } + if ($AllowedMaturities -notcontains $depMaturity) { continue } + if ($resolved[$capitalType].Add($dep)) { if ($type -eq 'agents') { $agentQueue.Enqueue($dep) @@ -1208,11 +1269,24 @@ function Invoke-PrepareExtension { # Load collection manifest if specified $collectionManifest = $null $collectionArtifactNames = $null + $collectionMaturities = @{} if ($Collection -and $Collection -ne "") { $collectionManifest = Get-CollectionManifest -CollectionPath $Collection Write-Host "Collection: $($collectionManifest.displayName) ($($collectionManifest.id))" + $artifactCollectionManifest = $collectionManifest + if (-not $artifactCollectionManifest.ContainsKey('items') -or @($artifactCollectionManifest.items).Count -eq 0) { + $rootCollectionPath = Join-Path $RepoRoot "collections/$($collectionManifest.id).collection.yml" + if (Test-Path $rootCollectionPath) { + $artifactCollectionManifest = ConvertFrom-Yaml -Yaml (Get-Content -Path $rootCollectionPath -Raw) + Write-Host "Using root collection metadata: $rootCollectionPath" + } + else { + Write-Warning "No root collection metadata found for '$($collectionManifest.id)' at $rootCollectionPath" + } + } + # Check collection-level maturity eligibility $collectionEligibility = Test-CollectionMaturityEligible -CollectionManifest $collectionManifest -Channel $Channel if (-not $collectionEligibility.IsEligible) { @@ -1223,8 +1297,27 @@ function Invoke-PrepareExtension { $collectionMaturity = if ($collectionManifest.ContainsKey('maturity')) { $collectionManifest['maturity'] } else { 'stable' } Write-Host "Collection maturity: $collectionMaturity" - # Get persona-filtered artifact names - $collectionArtifactNames = Get-CollectionArtifacts -Registry $registry -Collection $collectionManifest -AllowedMaturities $allowedMaturities + # Build collection maturity map and channel-filtered artifact names + $collectionMaturities = @{} + + if ($artifactCollectionManifest.ContainsKey('items')) { + foreach ($item in $artifactCollectionManifest.items) { + if (-not $item.ContainsKey('kind') -or -not $item.ContainsKey('path')) { + continue + } + + $itemKind = [string]$item.kind + $itemPath = [string]$item.path + $artifactKey = Get-CollectionArtifactKey -Kind $itemKind -Path $itemPath + $effectiveMaturity = Get-CollectionArtifactMaturity -CollectionItem $item -Registry $registry + if (-not $collectionMaturities.ContainsKey("${itemKind}s") -or $null -eq $collectionMaturities["${itemKind}s"]) { + $collectionMaturities["${itemKind}s"] = @{} + } + $collectionMaturities["${itemKind}s"][$artifactKey] = $effectiveMaturity + } + } + + $collectionArtifactNames = Get-CollectionArtifacts -Registry $registry -Collection $artifactCollectionManifest -AllowedMaturities $allowedMaturities # Resolve handoff dependencies (agents only) $agentsDir = Join-Path $GitHubDir "agents" @@ -1237,7 +1330,7 @@ function Invoke-PrepareExtension { prompts = $collectionArtifactNames.Prompts instructions = $collectionArtifactNames.Instructions skills = $collectionArtifactNames.Skills - } -Registry $registry -AllowedMaturities $allowedMaturities + } -Registry $registry -AllowedMaturities $allowedMaturities -CollectionMaturities $collectionMaturities $collectionArtifactNames = @{ Agents = $resolvedNames.Agents @@ -1247,9 +1340,16 @@ function Invoke-PrepareExtension { } } - # Discover agents + # Discover artifacts + $discoveryAllowedMaturities = if ($null -ne $collectionArtifactNames) { + @('stable', 'preview', 'experimental', 'deprecated') + } + else { + $allowedMaturities + } + $agentsDir = Join-Path $GitHubDir "agents" - $agentResult = Get-DiscoveredAgents -AgentsDir $agentsDir -AllowedMaturities $allowedMaturities -ExcludedAgents @() -Registry $registry + $agentResult = Get-DiscoveredAgents -AgentsDir $agentsDir -AllowedMaturities $discoveryAllowedMaturities -ExcludedAgents @() -Registry $registry $chatAgents = $agentResult.Agents $excludedAgents = $agentResult.Skipped @@ -1261,7 +1361,7 @@ function Invoke-PrepareExtension { # Discover prompts $promptsDir = Join-Path $GitHubDir "prompts" - $promptResult = Get-DiscoveredPrompts -PromptsDir $promptsDir -GitHubDir $GitHubDir -AllowedMaturities $allowedMaturities -Registry $registry + $promptResult = Get-DiscoveredPrompts -PromptsDir $promptsDir -GitHubDir $GitHubDir -AllowedMaturities $discoveryAllowedMaturities -Registry $registry $chatPrompts = $promptResult.Prompts $excludedPrompts = $promptResult.Skipped @@ -1273,7 +1373,7 @@ function Invoke-PrepareExtension { # Discover instructions $instructionsDir = Join-Path $GitHubDir "instructions" - $instructionResult = Get-DiscoveredInstructions -InstructionsDir $instructionsDir -GitHubDir $GitHubDir -AllowedMaturities $allowedMaturities -Registry $registry + $instructionResult = Get-DiscoveredInstructions -InstructionsDir $instructionsDir -GitHubDir $GitHubDir -AllowedMaturities $discoveryAllowedMaturities -Registry $registry $chatInstructions = $instructionResult.Instructions $excludedInstructions = $instructionResult.Skipped @@ -1285,7 +1385,7 @@ function Invoke-PrepareExtension { # Discover skills $skillsDir = Join-Path $GitHubDir "skills" - $skillResult = Get-DiscoveredSkills -SkillsDir $skillsDir -AllowedMaturities $allowedMaturities -Registry $registry + $skillResult = Get-DiscoveredSkills -SkillsDir $skillsDir -AllowedMaturities $discoveryAllowedMaturities -Registry $registry $chatSkills = $skillResult.Skills $excludedSkills = $skillResult.Skipped diff --git a/scripts/linting/schemas/collection-manifest.schema.json b/scripts/linting/schemas/collection-manifest.schema.json index e4a76bbf..21f4680f 100644 --- a/scripts/linting/schemas/collection-manifest.schema.json +++ b/scripts/linting/schemas/collection-manifest.schema.json @@ -44,6 +44,11 @@ "usage": { "type": "string", "description": "Optional usage guidance for the item." + }, + "maturity": { + "type": "string", + "enum": ["stable", "preview", "experimental", "deprecated"], + "description": "Optional release maturity for this item. Defaults to stable when omitted." } }, "additionalProperties": false diff --git a/scripts/plugins/Generate-Plugins.ps1 b/scripts/plugins/Generate-Plugins.ps1 index 0dcc0fcc..2730740c 100644 --- a/scripts/plugins/Generate-Plugins.ps1 +++ b/scripts/plugins/Generate-Plugins.ps1 @@ -24,6 +24,11 @@ .PARAMETER DryRun Optional. Shows what would be done without making changes. +.PARAMETER Channel + Optional. Release channel controlling eligible item maturities. + Stable includes only stable items. PreRelease includes stable, preview, + and experimental. Deprecated is excluded from both channels. + .EXAMPLE ./Generate-Plugins.ps1 # Generates all plugins (default: all + refresh) @@ -36,6 +41,10 @@ ./Generate-Plugins.ps1 -DryRun # Shows what would be generated without making changes +.EXAMPLE + ./Generate-Plugins.ps1 -Channel Stable + # Generates plugins with stable-only items + .NOTES Dependencies: PowerShell-Yaml module, scripts/plugins/Modules/PluginHelpers.psm1 #> @@ -49,7 +58,11 @@ param( [switch]$Refresh, [Parameter(Mandatory = $false)] - [switch]$DryRun + [switch]$DryRun, + + [Parameter(Mandatory = $false)] + [ValidateSet('Stable', 'PreRelease')] + [string]$Channel = 'PreRelease' ) $ErrorActionPreference = 'Stop' @@ -59,6 +72,76 @@ Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force #region Orchestration +function Get-AllowedCollectionMaturities { + <# + .SYNOPSIS + Returns allowed collection item maturities for a channel. + + .PARAMETER Channel + Release channel ('Stable' or 'PreRelease'). + + .OUTPUTS + [string[]] Allowed maturity values for collection items. + #> + [CmdletBinding()] + [OutputType([string[]])] + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Stable', 'PreRelease')] + [string]$Channel + ) + + if ($Channel -eq 'Stable') { + return @('stable') + } + + return @('stable', 'preview', 'experimental') +} + +function Select-CollectionItemsByChannel { + <# + .SYNOPSIS + Filters collection items by channel using item maturity metadata. + + .PARAMETER Collection + Collection manifest hashtable. + + .PARAMETER Channel + Release channel ('Stable' or 'PreRelease'). + + .OUTPUTS + [hashtable] Collection clone with filtered items. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$Collection, + + [Parameter(Mandatory = $true)] + [ValidateSet('Stable', 'PreRelease')] + [string]$Channel + ) + + $allowedMaturities = Get-AllowedCollectionMaturities -Channel $Channel + $filteredItems = @() + + foreach ($item in $Collection.items) { + $effectiveMaturity = Resolve-CollectionItemMaturity -Maturity $item.maturity + if ($allowedMaturities -contains $effectiveMaturity) { + $filteredItems += $item + } + } + + $filteredCollection = @{} + foreach ($key in $Collection.Keys) { + $filteredCollection[$key] = $Collection[$key] + } + $filteredCollection['items'] = $filteredItems + + return $filteredCollection +} + function Invoke-PluginGeneration { <# .SYNOPSIS @@ -82,6 +165,9 @@ function Invoke-PluginGeneration { .PARAMETER DryRun When specified, logs actions without creating files or directories. + .PARAMETER Channel + Release channel controlling item maturity eligibility. + .OUTPUTS Hashtable with Success, PluginCount, and ErrorMessage keys via New-GenerateResult. @@ -100,7 +186,11 @@ function Invoke-PluginGeneration { [switch]$Refresh, [Parameter(Mandatory = $false)] - [switch]$DryRun + [switch]$DryRun, + + [Parameter(Mandatory = $false)] + [ValidateSet('Stable', 'PreRelease')] + [string]$Channel = 'PreRelease' ) $collectionsDir = Join-Path -Path $RepoRoot -ChildPath 'collections' @@ -130,6 +220,7 @@ function Invoke-PluginGeneration { Write-Host "`n=== Plugin Generation ===" -ForegroundColor Cyan Write-Host "Collections: $($allCollections.Count)" + Write-Host "Channel: $Channel" Write-Host "Plugins dir: $pluginsDir" if ($DryRun) { Write-Host '[DRY RUN] No changes will be made' -ForegroundColor Yellow @@ -157,12 +248,14 @@ function Invoke-PluginGeneration { } # Generate plugin directory structure - $result = Write-PluginDirectory -Collection $collection ` + $filteredCollection = Select-CollectionItemsByChannel -Collection $collection -Channel $Channel + + $result = Write-PluginDirectory -Collection $filteredCollection ` -PluginsDir $pluginsDir ` -RepoRoot $RepoRoot ` -DryRun:$DryRun - $itemCount = $collection.items.Count + $itemCount = $filteredCollection.items.Count $totalAgents += $result.AgentCount $totalCommands += $result.CommandCount $totalInstructions += $result.InstructionCount @@ -210,7 +303,8 @@ if ($MyInvocation.InvocationName -ne '.') { -RepoRoot $RepoRoot ` -CollectionIds $CollectionIds ` -Refresh:$effectiveRefresh ` - -DryRun:$DryRun + -DryRun:$DryRun ` + -Channel $Channel if (-not $result.Success) { throw $result.ErrorMessage diff --git a/scripts/plugins/Modules/PluginHelpers.psm1 b/scripts/plugins/Modules/PluginHelpers.psm1 index efd31737..b3d1f95f 100644 --- a/scripts/plugins/Modules/PluginHelpers.psm1 +++ b/scripts/plugins/Modules/PluginHelpers.psm1 @@ -47,7 +47,7 @@ function Get-ArtifactFrontmatter { .DESCRIPTION Parses the YAML frontmatter block delimited by --- markers at the start - of a markdown file. Returns a hashtable with description and maturity keys. + of a markdown file. Returns a hashtable with description. .PARAMETER FilePath Path to the markdown file to parse. @@ -56,7 +56,7 @@ function Get-ArtifactFrontmatter { Default description if none found in frontmatter. .OUTPUTS - [hashtable] With description and maturity keys. + [hashtable] With description key. #> [CmdletBinding()] [OutputType([hashtable])] @@ -70,7 +70,6 @@ function Get-ArtifactFrontmatter { $content = Get-Content -Path $FilePath -Raw $description = '' - $maturity = 'stable' if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') { $yamlContent = $Matches[1] -replace '\r\n', "`n" -replace '\r', "`n" @@ -79,9 +78,6 @@ function Get-ArtifactFrontmatter { if ($data.ContainsKey('description')) { $description = $data.description } - if ($data.ContainsKey('maturity')) { - $maturity = $data.maturity - } } catch { Write-Warning "Failed to parse YAML frontmatter in $(Split-Path -Leaf $FilePath): $_" @@ -90,10 +86,40 @@ function Get-ArtifactFrontmatter { return @{ description = if ($description) { $description } else { $FallbackDescription } - maturity = $maturity } } +function Resolve-CollectionItemMaturity { + <# + .SYNOPSIS + Resolves effective maturity from collection item metadata. + + .DESCRIPTION + Returns stable when maturity is omitted; otherwise returns the provided + maturity string. + + .PARAMETER Maturity + Optional maturity value from a collection item. + + .OUTPUTS + [string] Effective maturity value. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter()] + [AllowNull()] + [AllowEmptyString()] + [string]$Maturity + ) + + if ([string]::IsNullOrWhiteSpace($Maturity)) { + return 'stable' + } + + return $Maturity +} + function Get-AllCollections { <# .SYNOPSIS @@ -153,33 +179,24 @@ function Get-ArtifactFiles { $items = @() - # Agents - $agentsDir = Join-Path -Path $RepoRoot -ChildPath '.github/agents' - if (Test-Path -Path $agentsDir) { - $agentFiles = Get-ChildItem -Path $agentsDir -Filter '*.agent.md' -File - foreach ($file in $agentFiles) { - $relativePath = [System.IO.Path]::GetRelativePath($RepoRoot, $file.FullName) -replace '\\', '/' - $items += @{ path = $relativePath; kind = 'agent' } + # Prompt-engineering artifacts discovered by ..md suffix under .github/ + # Keep explicit suffix mapping only where naming differs from manifest kind values. + $gitHubDir = Join-Path -Path $RepoRoot -ChildPath '.github' + if (Test-Path -Path $gitHubDir) { + $suffixToKind = @{ + instructions = 'instruction' } - } - # Prompts - $promptsDir = Join-Path -Path $RepoRoot -ChildPath '.github/prompts' - if (Test-Path -Path $promptsDir) { - $promptFiles = Get-ChildItem -Path $promptsDir -Filter '*.prompt.md' -File - foreach ($file in $promptFiles) { - $relativePath = [System.IO.Path]::GetRelativePath($RepoRoot, $file.FullName) -replace '\\', '/' - $items += @{ path = $relativePath; kind = 'prompt' } - } - } + $artifactFiles = Get-ChildItem -Path $gitHubDir -Filter '*.*.md' -File -Recurse + foreach ($file in $artifactFiles) { + if ($file.Name -notmatch '\.(?[^.]+)\.md$') { + continue + } - # Instructions (recursive for subfolders) - $instructionsDir = Join-Path -Path $RepoRoot -ChildPath '.github/instructions' - if (Test-Path -Path $instructionsDir) { - $instructionFiles = Get-ChildItem -Path $instructionsDir -Filter '*.instructions.md' -File -Recurse - foreach ($file in $instructionFiles) { + $suffix = $Matches['suffix'].ToLowerInvariant() + $kind = if ($suffixToKind.ContainsKey($suffix)) { $suffixToKind[$suffix] } else { $suffix } $relativePath = [System.IO.Path]::GetRelativePath($RepoRoot, $file.FullName) -replace '\\', '/' - $items += @{ path = $relativePath; kind = 'instruction' } + $items += @{ path = $relativePath; kind = $kind } } } @@ -202,20 +219,14 @@ function Get-ArtifactFiles { function Test-ArtifactDeprecated { <# .SYNOPSIS - Checks whether an artifact has maturity: deprecated in its frontmatter. + Checks whether an artifact has maturity deprecated in collection metadata. .DESCRIPTION - Reads the frontmatter of the artifact file (or SKILL.md for skills) and - returns $true when the maturity field equals deprecated. - - .PARAMETER ItemPath - Repo-relative path to the artifact. - - .PARAMETER Kind - The artifact kind: agent, prompt, instruction, or skill. + Reads maturity from the provided collection item metadata value and + returns $true when the effective value equals deprecated. - .PARAMETER RepoRoot - Absolute path to the repository root. + .PARAMETER Maturity + Optional maturity value from collection item metadata. .OUTPUTS [bool] True when the artifact is deprecated. @@ -223,30 +234,13 @@ function Test-ArtifactDeprecated { [CmdletBinding()] [OutputType([bool])] param( - [Parameter(Mandatory = $true)] - [string]$ItemPath, - - [Parameter(Mandatory = $true)] - [ValidateSet('agent', 'prompt', 'instruction', 'skill')] - [string]$Kind, - - [Parameter(Mandatory = $true)] - [string]$RepoRoot + [Parameter()] + [AllowNull()] + [AllowEmptyString()] + [string]$Maturity ) - if ($Kind -eq 'skill') { - $filePath = Join-Path -Path $RepoRoot -ChildPath $ItemPath -AdditionalChildPath 'SKILL.md' - } - else { - $filePath = Join-Path -Path $RepoRoot -ChildPath $ItemPath - } - - if (-not (Test-Path -Path $filePath)) { - return $false - } - - $frontmatter = Get-ArtifactFrontmatter -FilePath $filePath - return ($frontmatter.maturity -eq 'deprecated') + return ((Resolve-CollectionItemMaturity -Maturity $Maturity) -eq 'deprecated') } function Update-HveCoreAllCollection { @@ -288,29 +282,55 @@ function Update-HveCoreAllCollection { # Discover all artifacts $allItems = Get-ArtifactFiles -RepoRoot $RepoRoot - # Filter deprecated + # Filter deprecated based on existing collection item maturity metadata + $existingItemMaturities = @{} + foreach ($existingItem in $existing.items) { + $existingKey = "$($existingItem.kind)|$($existingItem.path)" + $existingItemMaturities[$existingKey] = Resolve-CollectionItemMaturity -Maturity $existingItem.maturity + } + $deprecatedCount = 0 $filteredItems = @() foreach ($item in $allItems) { - if (Test-ArtifactDeprecated -ItemPath $item.path -Kind $item.kind -RepoRoot $RepoRoot) { + $itemKey = "$($item.kind)|$($item.path)" + $itemMaturity = 'stable' + if ($existingItemMaturities.ContainsKey($itemKey)) { + $itemMaturity = $existingItemMaturities[$itemKey] + } + + if (Test-ArtifactDeprecated -Maturity $itemMaturity) { $deprecatedCount++ Write-Verbose "Excluding deprecated: $($item.path)" continue } - $filteredItems += $item + + $filteredItems += @{ + path = $item.path + kind = $item.kind + maturity = $itemMaturity + } } - # Sort: by kind order (agent, prompt, instruction, skill), then by path + # Sort: known kinds first, then any additional kinds, then by path $kindOrder = @{ 'agent' = 0; 'prompt' = 1; 'instruction' = 2; 'skill' = 3 } - $sortedItems = $filteredItems | Sort-Object { $kindOrder[$_.kind] }, { $_.path } + $sortedItems = $filteredItems | Sort-Object ` + { if ($kindOrder.ContainsKey($_.kind)) { $kindOrder[$_.kind] } else { 100 } }, ` + { $_.kind }, ` + { $_.path } # Build new items array as ordered hashtables for clean YAML output $newItems = @() foreach ($item in $sortedItems) { - $newItems += [ordered]@{ + $newItem = [ordered]@{ path = $item.path kind = $item.kind } + + if ((Resolve-CollectionItemMaturity -Maturity $item.maturity) -ne 'stable') { + $newItem['maturity'] = $item.maturity + } + + $newItems += $newItem } # Compute diff @@ -776,6 +796,7 @@ Export-ModuleMember -Function @( 'New-PluginManifestContent', 'New-PluginReadmeContent', 'New-RelativeSymlink', + 'Resolve-CollectionItemMaturity', 'Test-ArtifactDeprecated', 'Update-HveCoreAllCollection', 'Write-PluginDirectory' diff --git a/scripts/plugins/Validate-Collections.ps1 b/scripts/plugins/Validate-Collections.ps1 index 86aa0370..36422e45 100644 --- a/scripts/plugins/Validate-Collections.ps1 +++ b/scripts/plugins/Validate-Collections.ps1 @@ -88,6 +88,66 @@ function Test-KindSuffix { return '' } +function Resolve-ItemMaturity { + <# + .SYNOPSIS + Resolves an item's effective maturity value. + + .DESCRIPTION + Returns 'stable' when maturity is omitted; otherwise returns the + provided maturity string. + + .PARAMETER Maturity + Optional maturity string from collection item metadata. + + .OUTPUTS + [string] Effective maturity value. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter()] + [AllowNull()] + [string]$Maturity + ) + + if ([string]::IsNullOrWhiteSpace($Maturity)) { + return 'stable' + } + + return $Maturity +} + +function Get-CollectionItemKey { + <# + .SYNOPSIS + Builds a stable uniqueness key for collection items. + + .DESCRIPTION + Uses kind and path to identify the same artifact across collections. + + .PARAMETER Kind + Artifact kind. + + .PARAMETER ItemPath + Artifact path. + + .OUTPUTS + [string] Composite key. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [string]$Kind, + + [Parameter(Mandatory = $true)] + [string]$ItemPath + ) + + return "$Kind|$ItemPath" +} + #endregion Validation Helpers #region Orchestration @@ -130,6 +190,9 @@ function Invoke-CollectionValidation { $errorCount = 0 $seenIds = @{} $validatedCount = 0 + $allowedMaturities = @('stable', 'preview', 'experimental', 'deprecated') + $canonicalCollectionId = 'hve-core-all' + $itemOccurrences = @{} foreach ($file in $collectionFiles) { $manifest = Get-CollectionManifest -CollectionPath $file.FullName @@ -173,6 +236,11 @@ function Invoke-CollectionValidation { $itemPath = $item.path $kind = $item.kind $absolutePath = Join-Path -Path $RepoRoot -ChildPath $itemPath + $itemMaturity = $null + if ($item.ContainsKey('maturity')) { + $itemMaturity = [string]$item.maturity + } + $effectiveMaturity = Resolve-ItemMaturity -Maturity $itemMaturity # Path existence if (-not (Test-Path -Path $absolutePath)) { @@ -190,6 +258,25 @@ function Invoke-CollectionValidation { $fileErrors += "item missing 'kind': $itemPath" } + if (-not [string]::IsNullOrWhiteSpace($itemMaturity) -and ($allowedMaturities -notcontains $itemMaturity)) { + $fileErrors += "invalid maturity '$itemMaturity' for item '$itemPath' (allowed: $($allowedMaturities -join ', '))" + } + + if (-not [string]::IsNullOrWhiteSpace($itemPath) -and -not [string]::IsNullOrWhiteSpace($kind)) { + $itemKey = Get-CollectionItemKey -Kind $kind -ItemPath $itemPath + if (-not $itemOccurrences.ContainsKey($itemKey)) { + $itemOccurrences[$itemKey] = @() + } + + $itemOccurrences[$itemKey] += @{ + CollectionId = $id + CollectionFile = $file.Name + Kind = $kind + Path = $itemPath + Maturity = $effectiveMaturity + } + } + # Informational log for instruction items if ($kind -eq 'instruction') { Write-Verbose " instruction: $itemPath" @@ -210,6 +297,33 @@ function Invoke-CollectionValidation { $validatedCount++ } + foreach ($itemKey in $itemOccurrences.Keys) { + $occurrences = $itemOccurrences[$itemKey] + if ($occurrences.Count -le 1) { + continue + } + + $canonicalMatches = @($occurrences | Where-Object { $_.CollectionId -eq $canonicalCollectionId }) + if ($canonicalMatches.Count -eq 0) { + $sharedCollections = ($occurrences | ForEach-Object { $_.CollectionId } | Sort-Object -Unique) -join ', ' + Write-Host " FAIL shared item '$itemKey' exists in collections [$sharedCollections] but has no canonical entry in '$canonicalCollectionId'" -ForegroundColor Red + $errorCount++ + continue + } + + $canonical = $canonicalMatches[0] + foreach ($occurrence in $occurrences) { + if ($occurrence.CollectionId -eq $canonicalCollectionId) { + continue + } + + if ($occurrence.Maturity -ne $canonical.Maturity) { + Write-Host " FAIL maturity conflict for '$itemKey': canonical '$canonicalCollectionId'='$($canonical.Maturity)', '$($occurrence.CollectionId)'='$($occurrence.Maturity)'" -ForegroundColor Red + $errorCount++ + } + } + } + Write-Host '' Write-Host "$validatedCount collections validated, $errorCount errors" From ef166d9b35e00e06455dedfe32b22df1e5cd70ff Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 17:59:29 -0800 Subject: [PATCH 43/62] refactor(scripts): remove legacy registry validation surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - remove obsolete registry workflow, validator script, tests, and fixtures - drop lint:registry from npm scripts - fix extension prepare empty-agent handoff expansion - align prepare tests with collection item behavior ๐Ÿงน - Generated by Copilot --- .../artifact-registry-validation.yml | 78 - package.json | 1 - scripts/extension/Prepare-Extension.ps1 | 8 +- scripts/linting/Validate-ArtifactRegistry.ps1 | 923 ---------- .../ArtifactRegistry/circular-deps.json | 62 - .../extra-persona-properties.json | 18 - .../ArtifactRegistry/extra-properties.json | 41 - .../ArtifactRegistry/invalid-json.json | 4 - .../ArtifactRegistry/invalid-maturity.json | 26 - .../invalid-persona-format-in-artifact.json | 27 - .../ArtifactRegistry/invalid-persona-id.json | 20 - .../ArtifactRegistry/invalid-version.json | 16 - .../missing-artifact-fields.json | 26 - .../ArtifactRegistry/missing-fields.json | 3 - .../missing-persona-name.json | 15 - .../missing-personas-defs.json | 11 - .../ArtifactRegistry/no-requires.json | 26 - .../renamed-file-mismatch.json | 26 - .../undefined-persona-ref.json | 39 - .../ArtifactRegistry/unknown-dep-refs.json | 38 - .../ArtifactRegistry/valid-registry.json | 90 - .../extension/Prepare-Extension.Tests.ps1 | 51 +- .../Validate-ArtifactRegistry.Tests.ps1 | 1631 ----------------- 23 files changed, 40 insertions(+), 3140 deletions(-) delete mode 100644 .github/workflows/artifact-registry-validation.yml delete mode 100644 scripts/linting/Validate-ArtifactRegistry.ps1 delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/extra-persona-properties.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-format-in-artifact.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/missing-artifact-fields.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/missing-fields.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/missing-persona-name.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/missing-personas-defs.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/no-requires.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json delete mode 100644 scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json delete mode 100644 scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 diff --git a/.github/workflows/artifact-registry-validation.yml b/.github/workflows/artifact-registry-validation.yml deleted file mode 100644 index d3004e5e..00000000 --- a/.github/workflows/artifact-registry-validation.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Artifact Registry Validation (Fallback) - -on: - workflow_dispatch: - inputs: - soft-fail: - description: "Whether to continue on registry validation errors" - required: false - type: boolean - default: false - warnings-as-errors: - description: "Treat warnings as errors" - required: false - type: boolean - default: true - workflow_call: - inputs: - soft-fail: - description: "Whether to continue on registry validation errors" - required: false - type: boolean - default: false - warnings-as-errors: - description: "Treat warnings as errors" - required: false - type: boolean - default: true - -permissions: - contents: read - -jobs: - artifact-registry-validation: - name: Validate Artifact Registry (Fallback) - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 - with: - persist-credentials: false - - - name: Create logs directory - shell: pwsh - run: | - New-Item -ItemType Directory -Force -Path logs | Out-Null - - - name: Run Artifact Registry Validation - id: validate - shell: pwsh - run: | - Write-Warning 'Artifact registry validation is deprecated and retained as fallback only.' - $params = @{} - - if ('${{ inputs.warnings-as-errors }}' -eq 'true') { - $params['WarningsAsErrors'] = $true - } - - & scripts/linting/Validate-ArtifactRegistry.ps1 @params - continue-on-error: true - - - name: Upload registry validation results - if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4.4.3 - with: - name: registry-validation-results - path: logs/registry-validation-results.json - retention-days: 30 - - - name: Check results and fail if needed - if: ${{ !inputs.soft-fail }} - shell: pwsh - run: | - if ('${{ steps.validate.outcome }}' -eq 'failure') { - Write-Host "Artifact registry validation failed and soft-fail is false. Failing the job." - exit 1 - } diff --git a/package.json b/package.json index dd94081d..d122690f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "lint:md-links": "pwsh -File scripts/linting/Markdown-Link-Check.ps1", "lint:frontmatter": "pwsh -NoProfile -Command \"& './scripts/linting/Validate-MarkdownFrontmatter.ps1' -WarningsAsErrors -EnableSchemaValidation\"", "lint:collections-metadata": "pwsh -File scripts/plugins/Validate-Collections.ps1", - "lint:registry": "pwsh -NoProfile -Command \"& './scripts/linting/Validate-ArtifactRegistry.ps1' -WarningsAsErrors\"", "lint:version-consistency": "pwsh -NoProfile -Command \"./scripts/security/Test-ActionVersionConsistency.ps1 -FailOnMismatch\"", "lint:all": "npm run format:tables && npm run lint:md && npm run lint:ps && npm run lint:yaml && npm run lint:links && npm run lint:frontmatter && npm run lint:collections-metadata && npm run lint:version-consistency", "format:tables": "markdown-table-formatter \"**/*.md\"", diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index 74e71b42..fd84c242 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -1320,9 +1320,11 @@ function Invoke-PrepareExtension { $collectionArtifactNames = Get-CollectionArtifacts -Registry $registry -Collection $artifactCollectionManifest -AllowedMaturities $allowedMaturities # Resolve handoff dependencies (agents only) - $agentsDir = Join-Path $GitHubDir "agents" - $expandedAgents = Resolve-HandoffDependencies -SeedAgents $collectionArtifactNames.Agents -AgentsDir $agentsDir -AllowedMaturities $allowedMaturities -Registry $registry - $collectionArtifactNames.Agents = $expandedAgents + if (@($collectionArtifactNames.Agents).Count -gt 0) { + $agentsDir = Join-Path $GitHubDir "agents" + $expandedAgents = Resolve-HandoffDependencies -SeedAgents $collectionArtifactNames.Agents -AgentsDir $agentsDir -AllowedMaturities $allowedMaturities -Registry $registry + $collectionArtifactNames.Agents = $expandedAgents + } # Resolve requires dependencies $resolvedNames = Resolve-RequiresDependencies -ArtifactNames @{ diff --git a/scripts/linting/Validate-ArtifactRegistry.ps1 b/scripts/linting/Validate-ArtifactRegistry.ps1 deleted file mode 100644 index b9dd8633..00000000 --- a/scripts/linting/Validate-ArtifactRegistry.ps1 +++ /dev/null @@ -1,923 +0,0 @@ -๏ปฟ# Copyright (c) Microsoft Corporation. -# SPDX-License-Identifier: MIT - -# Validate-ArtifactRegistry.ps1 -# -# Purpose: Validates the AI Artifacts Registry against its schema and the filesystem -# Author: HVE Core Team - -#Requires -Version 7.0 - -<# -.SYNOPSIS - Validates the AI Artifacts Registry against its schema and the filesystem. - -.DESCRIPTION - Validates the `.github/ai-artifacts-registry.json` file by checking: - - JSON structure, required fields, and additional-property constraints - - JSON Schema validation (PowerShell 7.4+ with Test-Json -SchemaFile) - - Per-artifact required fields (maturity, personas, tags) - - Maturity enum values and persona reference format - - Persona ID format and reference validity - - Artifact file existence on disk - - Dependency reference validity - - Orphan file detection (files on disk not in registry) - -.PARAMETER RegistryPath - Path to the registry JSON file. Defaults to `.github/ai-artifacts-registry.json`. - -.PARAMETER RepoRoot - Repository root for resolving artifact file paths. Defaults to script's grandparent. - -.PARAMETER WarningsAsErrors - Treat warnings (orphan files) as errors. - -.PARAMETER OutputPath - Path to write JSON results. Defaults to `logs/registry-validation-results.json`. - -.OUTPUTS - Hashtable with Success bool, Errors array, Warnings array. - -.EXAMPLE - ./Validate-ArtifactRegistry.ps1 - # Validates registry with default paths - -.EXAMPLE - ./Validate-ArtifactRegistry.ps1 -WarningsAsErrors - # Treats orphan file warnings as errors -#> -[CmdletBinding()] -param( - [Parameter()] - [string]$RegistryPath, - - [Parameter()] - [string]$RepoRoot, - - [Parameter()] - [switch]$WarningsAsErrors, - - [Parameter()] - [string]$OutputPath -) - -$ErrorActionPreference = 'Stop' - -# Import CI helpers -Import-Module (Join-Path -Path $PSScriptRoot -ChildPath '../lib/Modules/CIHelpers.psm1') -Force - -#region Validation Functions - -function Test-RegistryStructure { - <# - .SYNOPSIS - Validates the registry JSON structure including required fields. - #> - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter(Mandatory)] - [string]$RegistryPath - ) - - $errors = [System.Collections.Generic.List[string]]::new() - - # JSON parse - try { - $content = Get-Content -Path $RegistryPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - } - catch { - $errors.Add("Failed to parse registry JSON: $_") - return @{ Success = $false; Errors = $errors; Registry = $null } - } - - # Required top-level fields - $requiredFields = @('$schema', 'version', 'personas', 'agents', 'prompts', 'instructions', 'skills') - foreach ($field in $requiredFields) { - if (-not $registry.ContainsKey($field)) { - $errors.Add("Missing required field: $field") - } - } - - # Version format - if ($registry.ContainsKey('version') -and $registry['version'] -notmatch '^\d+\.\d+$') { - $errors.Add("Invalid version format: $($registry['version']). Expected: major.minor") - } - - # Personas.definitions - if ($registry.ContainsKey('personas') -and -not $registry['personas'].ContainsKey('definitions')) { - $errors.Add("Missing required field: personas.definitions") - } - - # Additional properties - top level - $allowedTopLevel = @('$schema', 'version', 'personas', 'agents', 'prompts', 'instructions', 'skills') - foreach ($key in $registry.Keys) { - if ($key -notin $allowedTopLevel) { - $errors.Add("Unexpected top-level property: $key") - } - } - - # Additional properties - personas - if ($registry.ContainsKey('personas')) { - foreach ($key in $registry['personas'].Keys) { - if ($key -ne 'definitions') { - $errors.Add("Unexpected property in personas: $key") - } - } - } - - return @{ - Success = ($errors.Count -eq 0) - Errors = $errors - Registry = $registry - } -} - -function Test-PersonaReferences { - <# - .SYNOPSIS - Validates persona definitions and references in artifact entries. - #> - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter(Mandatory)] - [hashtable]$Registry - ) - - $errors = [System.Collections.Generic.List[string]]::new() - $definedPersonas = @($Registry['personas']['definitions'].Keys) - - # Validate persona definitions - foreach ($personaId in $definedPersonas) { - if ($personaId -notmatch '^[a-z][a-z0-9-]*$') { - $errors.Add("Invalid persona ID format: $personaId") - } - $persona = $Registry['personas']['definitions'][$personaId] - if (-not $persona.ContainsKey('name') -or [string]::IsNullOrEmpty($persona['name'])) { - $errors.Add("Persona '$personaId' missing 'name' field") - } - if (-not $persona.ContainsKey('description') -or [string]::IsNullOrEmpty($persona['description'])) { - $errors.Add("Persona '$personaId' missing 'description' field") - } - # Additional properties on persona definitions - $allowedPersonaProps = @('name', 'description') - foreach ($prop in $persona.Keys) { - if ($prop -notin $allowedPersonaProps) { - $errors.Add("Persona '$personaId' has unexpected property: $prop") - } - } - } - - # Validate persona references in artifacts - $sections = @('agents', 'prompts', 'instructions', 'skills') - foreach ($section in $sections) { - foreach ($key in $Registry[$section].Keys) { - $entry = $Registry[$section][$key] - if ($entry.ContainsKey('personas')) { - foreach ($personaRef in $entry['personas']) { - if ($personaRef -notin $definedPersonas) { - $errors.Add("${section}/${key} references undefined persona: $personaRef") - } - } - } - } - } - - return @{ Success = ($errors.Count -eq 0); Errors = $errors } -} - -function Test-ArtifactEntries { - <# - .SYNOPSIS - Validates per-artifact required fields, maturity enum, and property constraints. - #> - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter(Mandatory)] - [hashtable]$Registry - ) - - $errors = [System.Collections.Generic.List[string]]::new() - $validMaturity = @('stable', 'preview', 'experimental', 'deprecated') - $personaPattern = '^[a-z][a-z0-9-]*$' - - $simpleAllowed = @('maturity', 'personas', 'tags') - $agentAllowed = @('maturity', 'personas', 'tags', 'requires') - $requiresAllowed = @('agents', 'prompts', 'instructions', 'skills') - - $sectionAllowed = @{ - agents = $agentAllowed - prompts = $simpleAllowed - instructions = $simpleAllowed - skills = $simpleAllowed - } - - foreach ($section in $sectionAllowed.Keys) { - if (-not $Registry.ContainsKey($section)) { continue } - - foreach ($key in $Registry[$section].Keys) { - $entry = $Registry[$section][$key] - $prefix = "${section}/${key}" - - if ($entry -isnot [hashtable]) { - $errors.Add("${prefix}: entry must be an object") - continue - } - - # Required field: maturity - if (-not $entry.ContainsKey('maturity')) { - $errors.Add("${prefix}: missing required field 'maturity'") - } - elseif ($entry['maturity'] -notin $validMaturity) { - $errors.Add("${prefix}: invalid maturity '$($entry['maturity'])'. Must be one of: $($validMaturity -join ', ')") - } - - # Required field: personas - if (-not $entry.ContainsKey('personas')) { - $errors.Add("${prefix}: missing required field 'personas'") - } - elseif ($entry['personas'] -isnot [System.Collections.IList]) { - $errors.Add("${prefix}: 'personas' must be an array") - } - else { - foreach ($p in $entry['personas']) { - if ($p -notmatch $personaPattern) { - $errors.Add("${prefix}: invalid persona reference format '$p'. Must match: $personaPattern") - } - } - } - - # Required field: tags - if (-not $entry.ContainsKey('tags')) { - $errors.Add("${prefix}: missing required field 'tags'") - } - elseif ($entry['tags'] -isnot [System.Collections.IList]) { - $errors.Add("${prefix}: 'tags' must be an array") - } - - # No unexpected properties - $allowed = $sectionAllowed[$section] - foreach ($prop in $entry.Keys) { - if ($prop -notin $allowed) { - $errors.Add("${prefix}: unexpected property '$prop'") - } - } - - # Agent requires block structure - if ($section -eq 'agents' -and $entry.ContainsKey('requires')) { - $requires = $entry['requires'] - if ($requires -isnot [hashtable]) { - $errors.Add("${prefix}: 'requires' must be an object") - } - else { - foreach ($reqProp in $requires.Keys) { - if ($reqProp -notin $requiresAllowed) { - $errors.Add("${prefix}: unexpected property in requires: '$reqProp'") - } - elseif ($requires[$reqProp] -isnot [System.Collections.IList]) { - $errors.Add("${prefix}: requires.$reqProp must be an array") - } - } - } - } - } - } - - return @{ Success = ($errors.Count -eq 0); Errors = $errors } -} - -function Test-JsonSchemaValidation { - <# - .SYNOPSIS - Validates registry JSON against the schema file using Test-Json when available. - .DESCRIPTION - Requires PowerShell 7.4+ for Test-Json -SchemaFile support. Skips gracefully - on older versions. - #> - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter(Mandatory)] - [string]$RegistryPath, - - [Parameter(Mandatory)] - [string]$SchemaPath - ) - - $errors = [System.Collections.Generic.List[string]]::new() - - if (-not (Test-Path $SchemaPath)) { - $errors.Add("Schema file not found: $SchemaPath") - return @{ Success = $false; Errors = $errors } - } - - if ($PSVersionTable.PSVersion -lt [version]'7.4') { - Write-Verbose "Skipping JSON Schema validation: requires PowerShell 7.4+ (current: $($PSVersionTable.PSVersion))" - return @{ Success = $true; Errors = $errors } - } - - try { - $content = Get-Content -Path $RegistryPath -Raw - $schemaErrors = $null - $valid = Test-Json -Json $content -SchemaFile $SchemaPath -ErrorAction SilentlyContinue -ErrorVariable schemaErrors - if (-not $valid) { - foreach ($schemaErr in $schemaErrors) { - $errors.Add("Schema violation: $schemaErr") - } - if ($errors.Count -eq 0) { - $errors.Add("Registry does not conform to JSON Schema") - } - } - } - catch { - $errors.Add("JSON Schema validation error: $($_.Exception.Message)") - } - - return @{ Success = ($errors.Count -eq 0); Errors = $errors } -} - -function Get-ArtifactPath { - <# - .SYNOPSIS - Resolves the file path for an artifact based on section and key. - #> - [CmdletBinding()] - [OutputType([string])] - param( - [Parameter(Mandatory)] - [string]$Section, - - [Parameter(Mandatory)] - [string]$Key, - - [Parameter(Mandatory)] - [string]$RepoRoot - ) - - switch ($Section) { - 'agents' { - return Join-Path $RepoRoot ".github/agents/$Key.agent.md" - } - 'prompts' { - return Join-Path $RepoRoot ".github/prompts/$Key.prompt.md" - } - 'instructions' { - return Join-Path $RepoRoot ".github/instructions/$Key.instructions.md" - } - 'skills' { - return Join-Path $RepoRoot ".github/skills/$Key/SKILL.md" - } - default { - return $null - } - } -} - -function Test-ArtifactFileExistence { - <# - .SYNOPSIS - Validates that each artifact key maps to an existing file on disk. - #> - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter(Mandatory)] - [hashtable]$Registry, - - [Parameter(Mandatory)] - [string]$RepoRoot - ) - - $errors = [System.Collections.Generic.List[string]]::new() - $sections = @('agents', 'prompts', 'instructions', 'skills') - - foreach ($section in $sections) { - foreach ($key in $Registry[$section].Keys) { - $path = Get-ArtifactPath -Section $section -Key $key -RepoRoot $RepoRoot - if (-not (Test-Path $path)) { - $errors.Add("${section}/${key}: File not found at $path") - } - } - } - - return @{ Success = ($errors.Count -eq 0); Errors = $errors } -} - -function Test-DependencyReferences { - <# - .SYNOPSIS - Validates that dependency references point to existing registry entries. - #> - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter(Mandatory)] - [hashtable]$Registry - ) - - $errors = [System.Collections.Generic.List[string]]::new() - $warnings = [System.Collections.Generic.List[string]]::new() - - # Build reference sets - $validAgents = [System.Collections.Generic.HashSet[string]]::new([string[]]@($Registry['agents'].Keys)) - $validPrompts = [System.Collections.Generic.HashSet[string]]::new([string[]]@($Registry['prompts'].Keys)) - $validInstructions = [System.Collections.Generic.HashSet[string]]::new([string[]]@($Registry['instructions'].Keys)) - $validSkills = [System.Collections.Generic.HashSet[string]]::new([string[]]@($Registry['skills'].Keys)) - - # Only agents have requires blocks (by design) - foreach ($agentKey in $Registry['agents'].Keys) { - $agent = $Registry['agents'][$agentKey] - if (-not $agent.ContainsKey('requires')) { continue } - - $requires = $agent['requires'] - - if ($requires.ContainsKey('agents')) { - foreach ($ref in $requires['agents']) { - if (-not $validAgents.Contains($ref)) { - $errors.Add("agents/${agentKey} requires.agents references unknown agent: $ref") - } - } - } - - if ($requires.ContainsKey('prompts')) { - foreach ($ref in $requires['prompts']) { - if (-not $validPrompts.Contains($ref)) { - $errors.Add("agents/${agentKey} requires.prompts references unknown prompt: $ref") - } - } - } - - if ($requires.ContainsKey('instructions')) { - foreach ($ref in $requires['instructions']) { - if (-not $validInstructions.Contains($ref)) { - $errors.Add("agents/${agentKey} requires.instructions references unknown instruction: $ref") - } - } - } - - if ($requires.ContainsKey('skills')) { - foreach ($ref in $requires['skills']) { - if (-not $validSkills.Contains($ref)) { - $errors.Add("agents/${agentKey} requires.skills references unknown skill: $ref") - } - } - } - } - - # Detect circular agent dependencies (warning only) - $circularChains = Find-CircularAgentDependencies -Registry $Registry - foreach ($chain in $circularChains) { - $warnings.Add("Circular agent dependency detected: $($chain -join ' -> ')") - } - - return @{ Success = ($errors.Count -eq 0); Errors = $errors; Warnings = $warnings } -} - -function Find-CircularAgentDependencies { - <# - .SYNOPSIS - Detects circular dependencies in agent requires.agents chains. - #> - [CmdletBinding()] - [OutputType([System.Collections.Generic.List[string[]]])] - param( - [Parameter(Mandatory)] - [hashtable]$Registry - ) - - $chains = [System.Collections.Generic.List[string[]]]::new() - $globalVisited = @{} - - foreach ($agent in $Registry['agents'].Keys) { - $path = [System.Collections.Generic.List[string]]::new() - $localVisited = @{} - Find-CycleFromAgent -Registry $Registry -Agent $agent -Path $path -LocalVisited $localVisited -GlobalVisited $globalVisited -Chains $chains - } - - return $chains -} - -function Find-CycleFromAgent { - param( - [hashtable]$Registry, - [string]$Agent, - [System.Collections.Generic.List[string]]$Path, - [hashtable]$LocalVisited, - [hashtable]$GlobalVisited, - [System.Collections.Generic.List[string[]]]$Chains - ) - - if ($LocalVisited.ContainsKey($Agent)) { - $cycleStart = $Path.IndexOf($Agent) - if ($cycleStart -ge 0) { - $cycle = @($Path[$cycleStart..($Path.Count - 1)]) + @($Agent) - $cycleKey = ($cycle | Sort-Object) -join ',' - if (-not $GlobalVisited.ContainsKey($cycleKey)) { - $GlobalVisited[$cycleKey] = $true - $Chains.Add($cycle) - } - } - return - } - - $LocalVisited[$Agent] = $true - $Path.Add($Agent) - - $entry = $Registry['agents'][$Agent] - if ($entry -and $entry.ContainsKey('requires') -and $entry['requires'].ContainsKey('agents')) { - foreach ($dep in $entry['requires']['agents']) { - if ($Registry['agents'].ContainsKey($dep)) { - Find-CycleFromAgent -Registry $Registry -Agent $dep -Path $Path -LocalVisited $LocalVisited -GlobalVisited $GlobalVisited -Chains $Chains - } - } - } - - $Path.RemoveAt($Path.Count - 1) - $LocalVisited.Remove($Agent) -} - -function Find-OrphanArtifacts { - <# - .SYNOPSIS - Detects artifact files on disk that are not registered in the registry. - #> - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter(Mandatory)] - [hashtable]$Registry, - - [Parameter(Mandatory)] - [string]$RepoRoot - ) - - $warnings = [System.Collections.Generic.List[string]]::new() - - # Scan agents - $agentsDir = Join-Path $RepoRoot '.github/agents' - if (Test-Path $agentsDir) { - $agentFiles = Get-ChildItem -Path $agentsDir -Filter '*.agent.md' -File -ErrorAction SilentlyContinue - foreach ($file in $agentFiles) { - $key = $file.BaseName -replace '\.agent$', '' - if (-not $Registry['agents'].ContainsKey($key)) { - $warnings.Add("Orphan agent file not in registry: $($file.FullName)") - } - } - } - - # Scan prompts - $promptsDir = Join-Path $RepoRoot '.github/prompts' - if (Test-Path $promptsDir) { - $promptFiles = Get-ChildItem -Path $promptsDir -Filter '*.prompt.md' -File -Recurse -ErrorAction SilentlyContinue - foreach ($file in $promptFiles) { - $key = $file.BaseName -replace '\.prompt$', '' - if (-not $Registry['prompts'].ContainsKey($key)) { - $warnings.Add("Orphan prompt file not in registry: $($file.FullName)") - } - } - } - - # Scan instructions (including subdirectories, excluding repo-specific hve-core/ folder) - $instructionsDir = Join-Path $RepoRoot '.github/instructions' - if (Test-Path $instructionsDir) { - $instructionFiles = Get-ChildItem -Path $instructionsDir -Filter '*.instructions.md' -File -Recurse -ErrorAction SilentlyContinue - foreach ($file in $instructionFiles) { - $relativePath = [System.IO.Path]::GetRelativePath($instructionsDir, $file.FullName) -replace '\\', '/' - # Skip repo-specific instructions not intended for distribution - if ($relativePath -like 'hve-core/*') { continue } - $key = $relativePath -replace '\.instructions\.md$', '' - if (-not $Registry['instructions'].ContainsKey($key)) { - $warnings.Add("Orphan instruction file not in registry: $($file.FullName)") - } - } - } - - # Scan skills - $skillsDir = Join-Path $RepoRoot '.github/skills' - if (Test-Path $skillsDir) { - $skillDirs = Get-ChildItem -Path $skillsDir -Directory -ErrorAction SilentlyContinue - foreach ($dir in $skillDirs) { - $skillFile = Join-Path $dir.FullName 'SKILL.md' - if (Test-Path $skillFile) { - $key = $dir.Name - if (-not $Registry['skills'].ContainsKey($key)) { - $warnings.Add("Orphan skill directory not in registry: $($dir.FullName)") - } - } - } - } - - return @{ Warnings = $warnings } -} - -function Test-CollectionManifests { - <# - .SYNOPSIS - Validates collection manifest files against their schema and registry. - .DESCRIPTION - Checks all collection manifest files in extension/collections/ for: - - Valid JSON structure - - Valid maturity enum values - - Persona references matching registry definitions - - JSON Schema validation (PowerShell 7.4+) - #> - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter(Mandatory)] - [hashtable]$Registry, - - [Parameter(Mandatory)] - [string]$RepoRoot - ) - - $errors = [System.Collections.Generic.List[string]]::new() - $warnings = [System.Collections.Generic.List[string]]::new() - - $collectionsDir = Join-Path $RepoRoot 'extension/collections' - if (-not (Test-Path $collectionsDir)) { - return @{ Success = $true; Errors = $errors; Warnings = $warnings } - } - - $validMaturity = @('stable', 'preview', 'experimental', 'deprecated') - $definedPersonas = @() - if ($Registry.ContainsKey('personas') -and $Registry['personas'].ContainsKey('definitions')) { - $definedPersonas = @($Registry['personas']['definitions'].Keys) - } - - $collectionFiles = Get-ChildItem -Path $collectionsDir -Filter '*.collection.json' -File -ErrorAction SilentlyContinue - - # JSON Schema validation for collection manifests - $collectionSchemaPath = Join-Path $RepoRoot 'scripts/linting/schemas/collection.schema.json' - - foreach ($file in $collectionFiles) { - $prefix = "collection/$($file.BaseName -replace '\.collection$', '')" - - try { - $content = Get-Content -Path $file.FullName -Raw - $manifest = $content | ConvertFrom-Json -AsHashtable - } - catch { - $errors.Add("${prefix}: Failed to parse JSON: $_") - continue - } - - # Validate maturity value if present - if ($manifest.ContainsKey('maturity')) { - if ($manifest['maturity'] -notin $validMaturity) { - $errors.Add("${prefix}: invalid maturity '$($manifest['maturity'])'. Must be one of: $($validMaturity -join ', ')") - } - } - - # Validate persona references - if ($manifest.ContainsKey('personas')) { - foreach ($persona in $manifest['personas']) { - if ($definedPersonas.Count -gt 0 -and $persona -notin $definedPersonas -and $persona -ne 'hve-core-all') { - $warnings.Add("${prefix}: references persona '$persona' not defined in registry") - } - } - } - - # Warn about deprecated collections that still exist in the build directory - if ($manifest.ContainsKey('maturity') -and $manifest['maturity'] -eq 'deprecated') { - $warnings.Add("${prefix}: collection is deprecated and will be excluded from all builds") - } - - # JSON Schema validation (PowerShell 7.4+) - if ((Test-Path $collectionSchemaPath) -and $PSVersionTable.PSVersion -ge [version]'7.4') { - try { - $schemaErrors = $null - $valid = Test-Json -Json $content -SchemaFile $collectionSchemaPath -ErrorAction SilentlyContinue -ErrorVariable schemaErrors - if (-not $valid) { - foreach ($schemaErr in $schemaErrors) { - $errors.Add("${prefix}: schema violation: $schemaErr") - } - } - } - catch { - $errors.Add("${prefix}: schema validation error: $($_.Exception.Message)") - } - } - } - - return @{ Success = ($errors.Count -eq 0); Errors = $errors; Warnings = $warnings } -} - -#endregion Validation Functions - -#region Output Functions - -function Write-RegistryValidationOutput { - <# - .SYNOPSIS - Writes validation results to console with formatting. - #> - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [hashtable]$Result, - - [Parameter(Mandatory)] - [string]$RegistryPath - ) - - Write-Host "`n๐Ÿ” Registry Validation Results" -ForegroundColor Cyan - Write-Host "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" -ForegroundColor DarkGray - Write-Host " Registry: $RegistryPath" - - if ($Result.Errors.Count -gt 0) { - Write-Host "`nโŒ Errors ($($Result.Errors.Count)):" -ForegroundColor Red - foreach ($errorItem in $Result.Errors) { - Write-Host " โ€ข $errorItem" -ForegroundColor Red - } - } - - if ($Result.Warnings.Count -gt 0) { - Write-Host "`nโš ๏ธ Warnings ($($Result.Warnings.Count)):" -ForegroundColor Yellow - foreach ($warningItem in $Result.Warnings) { - Write-Host " โ€ข $warningItem" -ForegroundColor Yellow - } - } - - Write-Host "`n๐Ÿ“Š Summary:" -ForegroundColor Cyan - $errorColor = if ($Result.Errors.Count -gt 0) { 'Red' } else { 'Green' } - $warnColor = if ($Result.Warnings.Count -gt 0) { 'Yellow' } else { 'Green' } - Write-Host " Errors: $($Result.Errors.Count)" -ForegroundColor $errorColor - Write-Host " Warnings: $($Result.Warnings.Count)" -ForegroundColor $warnColor - - if ($Result.ArtifactCounts) { - Write-Host " Agents: $($Result.ArtifactCounts.Agents)" - Write-Host " Prompts: $($Result.ArtifactCounts.Prompts)" - Write-Host " Instructions: $($Result.ArtifactCounts.Instructions)" - Write-Host " Skills: $($Result.ArtifactCounts.Skills)" - } -} - -function Export-RegistryValidationResults { - <# - .SYNOPSIS - Exports validation results to JSON file. - #> - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [hashtable]$Result, - - [Parameter(Mandatory)] - [string]$OutputPath - ) - - $outputDir = Split-Path -Path $OutputPath -Parent - if ($outputDir -and -not (Test-Path $outputDir)) { - New-Item -ItemType Directory -Path $outputDir -Force | Out-Null - } - - $exportData = @{ - timestamp = (Get-Date -Format 'o') - success = $Result.Success - errors = $Result.Errors - warnings = $Result.Warnings - artifactCounts = $Result.ArtifactCounts - } - - $exportData | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath -Encoding utf8 -} - -#endregion Output Functions - -#region Main Execution - -try { - if ($MyInvocation.InvocationName -ne '.') { - # Resolve paths - script lives at scripts/linting/, so grandparent is repo root - if (-not $RepoRoot) { - $RepoRoot = (Resolve-Path "$PSScriptRoot/../..").Path - } - - if (-not $RegistryPath) { - $RegistryPath = Join-Path $RepoRoot '.github/ai-artifacts-registry.json' - } - - if (-not $OutputPath) { - $OutputPath = Join-Path $RepoRoot 'logs/registry-validation-results.json' - } - - Write-Host "๐Ÿ” Validating AI Artifacts Registry..." -ForegroundColor Cyan - - # Validate file exists - if (-not (Test-Path $RegistryPath)) { - throw "Registry file not found: $RegistryPath" - } - - # Run validations - $allErrors = [System.Collections.Generic.List[string]]::new() - $allWarnings = [System.Collections.Generic.List[string]]::new() - - # Step 1: Structure validation - $structureResult = Test-RegistryStructure -RegistryPath $RegistryPath - $allErrors.AddRange($structureResult.Errors) - - if (-not $structureResult.Success) { - # Cannot continue without valid structure - $result = @{ - Success = $false - Errors = $allErrors - Warnings = $allWarnings - ArtifactCounts = $null - } - } - else { - $registry = $structureResult.Registry - - # Step 2: Persona references - $personaResult = Test-PersonaReferences -Registry $registry - $allErrors.AddRange($personaResult.Errors) - - # Step 3: Artifact entry validation (maturity, personas, tags, additionalProperties) - $entryResult = Test-ArtifactEntries -Registry $registry - $allErrors.AddRange($entryResult.Errors) - - # Step 4: JSON Schema validation (PowerShell 7.4+) - $schemaPath = Join-Path $PSScriptRoot 'schemas/ai-artifacts-registry.schema.json' - $schemaResult = Test-JsonSchemaValidation -RegistryPath $RegistryPath -SchemaPath $schemaPath - $allErrors.AddRange($schemaResult.Errors) - - # Step 5: File existence - $fileResult = Test-ArtifactFileExistence -Registry $registry -RepoRoot $RepoRoot - $allErrors.AddRange($fileResult.Errors) - - # Step 6: Dependency references - $depResult = Test-DependencyReferences -Registry $registry - $allErrors.AddRange($depResult.Errors) - $allWarnings.AddRange($depResult.Warnings) - - # Step 7: Orphan detection - $orphanResult = Find-OrphanArtifacts -Registry $registry -RepoRoot $RepoRoot - $allWarnings.AddRange($orphanResult.Warnings) - - # Step 8: Collection manifest validation - $collectionResult = Test-CollectionManifests -Registry $registry -RepoRoot $RepoRoot - $allErrors.AddRange($collectionResult.Errors) - $allWarnings.AddRange($collectionResult.Warnings) - - # Build result - $result = @{ - Success = ($allErrors.Count -eq 0) - Errors = $allErrors - Warnings = $allWarnings - ArtifactCounts = @{ - Agents = $registry['agents'].Count - Prompts = $registry['prompts'].Count - Instructions = $registry['instructions'].Count - Skills = $registry['skills'].Count - } - } - } - - # Output - Write-RegistryValidationOutput -Result $result -RegistryPath $RegistryPath - - # CI annotations - if (Test-CIEnvironment) { - foreach ($errItem in $result.Errors) { - Write-CIAnnotation -Message $errItem -Level Error -File $RegistryPath - } - foreach ($warnItem in $result.Warnings) { - Write-CIAnnotation -Message $warnItem -Level Warning -File $RegistryPath - } - } - - # Export results - Export-RegistryValidationResults -Result $result -OutputPath $OutputPath - - # Exit code - $exitCode = 0 - if ($result.Errors.Count -gt 0) { - $exitCode = 1 - } - elseif ($WarningsAsErrors -and $result.Warnings.Count -gt 0) { - $exitCode = 1 - } - - if ($exitCode -eq 0) { - Write-Host "`nโœ… Registry validation passed!" -ForegroundColor Green - } - else { - Write-Host "`nโŒ Registry validation failed!" -ForegroundColor Red - } - - exit $exitCode - } -} -catch { - Write-Error -ErrorAction Continue "Registry validation failed: $($_.Exception.Message)" - if (Test-CIEnvironment) { - Write-CIAnnotation -Message "Registry validation failed: $($_.Exception.Message)" -Level Error - } - exit 1 -} - -#endregion diff --git a/scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json b/scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json deleted file mode 100644 index 805d7144..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/circular-deps.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "developer": { - "name": "Developer", - "description": "Software engineers" - } - } - }, - "agents": { - "agent-a": { - "maturity": "stable", - "personas": [ - "developer" - ], - "tags": [], - "requires": { - "agents": [ - "agent-b" - ], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "agent-b": { - "maturity": "stable", - "personas": [ - "developer" - ], - "tags": [], - "requires": { - "agents": [ - "agent-c" - ], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "agent-c": { - "maturity": "stable", - "personas": [ - "developer" - ], - "tags": [], - "requires": { - "agents": [ - "agent-a" - ], - "prompts": [], - "instructions": [], - "skills": [] - } - } - }, - "prompts": {}, - "instructions": {}, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/extra-persona-properties.json b/scripts/tests/Fixtures/ArtifactRegistry/extra-persona-properties.json deleted file mode 100644 index dd5f7872..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/extra-persona-properties.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "developer": { - "name": "Developer", - "description": "Software engineers", - "extraField": "not-allowed" - } - }, - "extraSection": "not-allowed" - }, - "agents": {}, - "prompts": {}, - "instructions": {}, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json b/scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json deleted file mode 100644 index 0b0de56f..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/extra-properties.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "developer": { - "name": "Developer", - "description": "Software engineers" - } - } - }, - "agents": { - "extra-agent": { - "maturity": "stable", - "personas": [ - "developer" - ], - "tags": [ - "test" - ], - "customField": true - } - }, - "prompts": { - "extra-prompt": { - "maturity": "stable", - "personas": [ - "developer" - ], - "tags": [ - "test" - ], - "requires": { - "agents": [] - } - } - }, - "instructions": {}, - "skills": {}, - "extraTopLevel": "should-not-be-here" -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json deleted file mode 100644 index a1980f1d..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/invalid-json.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0""malformed JSON - missing comma after version" -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json deleted file mode 100644 index cf62db4a..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/invalid-maturity.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "developer": { - "name": "Developer", - "description": "Software engineers" - } - } - }, - "agents": { - "bad-agent": { - "maturity": "INVALID", - "personas": [ - "developer" - ], - "tags": [ - "test" - ] - } - }, - "prompts": {}, - "instructions": {}, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-format-in-artifact.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-format-in-artifact.json deleted file mode 100644 index 0173d301..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-format-in-artifact.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "developer": { - "name": "Developer", - "description": "Software engineers" - } - } - }, - "agents": { - "bad-persona-ref": { - "maturity": "stable", - "personas": [ - "Bad Format!", - "123-invalid" - ], - "tags": [ - "test" - ] - } - }, - "prompts": {}, - "instructions": {}, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json deleted file mode 100644 index 99e7335d..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/invalid-persona-id.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "Invalid_ID_Format": { - "name": "Invalid Persona", - "description": "This persona ID uses uppercase and underscores" - }, - "123-starts-with-number": { - "name": "Bad Start", - "description": "This persona ID starts with a number" - } - } - }, - "agents": {}, - "prompts": {}, - "instructions": {}, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json b/scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json deleted file mode 100644 index e3023d5e..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/invalid-version.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0.0", - "personas": { - "definitions": { - "developer": { - "name": "Developer", - "description": "Software engineers" - } - } - }, - "agents": {}, - "prompts": {}, - "instructions": {}, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/missing-artifact-fields.json b/scripts/tests/Fixtures/ArtifactRegistry/missing-artifact-fields.json deleted file mode 100644 index 716263ab..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/missing-artifact-fields.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "developer": { - "name": "Developer", - "description": "Software engineers" - } - } - }, - "agents": {}, - "prompts": { - "bad-prompt": { - "tags": [ - "test" - ] - } - }, - "instructions": { - "bad-instruction": { - "maturity": "stable" - } - }, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/missing-fields.json b/scripts/tests/Fixtures/ArtifactRegistry/missing-fields.json deleted file mode 100644 index 9f15a245..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/missing-fields.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "description": "Registry missing required top-level fields" -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/missing-persona-name.json b/scripts/tests/Fixtures/ArtifactRegistry/missing-persona-name.json deleted file mode 100644 index 3998c0e9..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/missing-persona-name.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "incomplete-persona": { - "description": "This persona is missing the name field" - } - } - }, - "agents": {}, - "prompts": {}, - "instructions": {}, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/missing-personas-defs.json b/scripts/tests/Fixtures/ArtifactRegistry/missing-personas-defs.json deleted file mode 100644 index 864a985f..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/missing-personas-defs.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "note": "Missing definitions key" - }, - "agents": {}, - "prompts": {}, - "instructions": {}, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/no-requires.json b/scripts/tests/Fixtures/ArtifactRegistry/no-requires.json deleted file mode 100644 index 9d692ee9..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/no-requires.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "developer": { - "name": "Developer", - "description": "Software engineers" - } - } - }, - "agents": { - "standalone-agent": { - "maturity": "stable", - "personas": [ - "developer" - ], - "tags": [ - "standalone" - ] - } - }, - "prompts": {}, - "instructions": {}, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json b/scripts/tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json deleted file mode 100644 index add174f4..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/renamed-file-mismatch.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "developer": { - "name": "Developer", - "description": "Software engineers building applications" - } - } - }, - "agents": { - "old-agent-name": { - "maturity": "stable", - "personas": [ - "developer" - ], - "tags": [ - "test" - ] - } - }, - "prompts": {}, - "instructions": {}, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json b/scripts/tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json deleted file mode 100644 index e2656831..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/undefined-persona-ref.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "developer": { - "name": "Developer", - "description": "Software engineers" - } - } - }, - "agents": { - "bad-agent": { - "maturity": "stable", - "personas": [ - "nonexistent-persona", - "also-undefined" - ], - "tags": [], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - } - }, - "prompts": { - "bad-prompt": { - "maturity": "stable", - "personas": [ - "undefined-persona" - ], - "tags": [] - } - }, - "instructions": {}, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json b/scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json deleted file mode 100644 index a7d16518..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/unknown-dep-refs.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "developer": { - "name": "Developer", - "description": "Software engineers" - } - } - }, - "agents": { - "broken-agent": { - "maturity": "stable", - "personas": [ - "developer" - ], - "tags": [], - "requires": { - "agents": [ - "nonexistent-agent" - ], - "prompts": [ - "nonexistent-prompt" - ], - "instructions": [ - "nonexistent-instruction" - ], - "skills": [ - "nonexistent-skill" - ] - } - } - }, - "prompts": {}, - "instructions": {}, - "skills": {} -} \ No newline at end of file diff --git a/scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json b/scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json deleted file mode 100644 index 39ecb45c..00000000 --- a/scripts/tests/Fixtures/ArtifactRegistry/valid-registry.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "$schema": "../../../linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "developer": { - "name": "Developer", - "description": "Software engineers building applications" - }, - "devops": { - "name": "DevOps Engineer", - "description": "Infrastructure and deployment specialists" - } - } - }, - "agents": { - "test-agent": { - "maturity": "stable", - "personas": [ - "developer" - ], - "tags": [ - "test" - ], - "requires": { - "agents": [], - "prompts": [ - "test-prompt" - ], - "instructions": [ - "test-instruction" - ], - "skills": [] - } - }, - "dependent-agent": { - "maturity": "stable", - "personas": [ - "developer", - "devops" - ], - "tags": [ - "test" - ], - "requires": { - "agents": [ - "test-agent" - ], - "prompts": [], - "instructions": [], - "skills": [ - "test-skill" - ] - } - } - }, - "prompts": { - "test-prompt": { - "maturity": "stable", - "personas": [ - "developer" - ], - "tags": [ - "test" - ] - } - }, - "instructions": { - "test-instruction": { - "maturity": "stable", - "personas": [ - "developer" - ], - "tags": [ - "test" - ] - } - }, - "skills": { - "test-skill": { - "maturity": "stable", - "personas": [ - "devops" - ], - "tags": [ - "test" - ] - } - } -} \ No newline at end of file diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index 75887be4..ba4e05a6 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -488,37 +488,56 @@ Describe 'Get-CollectionArtifacts' { } } - It 'Filters by persona' { - $collection = @{ personas = @('developer') } + It 'Returns artifacts from collection items across supported kinds' { + $collection = @{ + items = @( + @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md' }, + @{ kind = 'prompt'; path = '.github/prompts/dev-prompt.prompt.md' }, + @{ kind = 'instruction'; path = '.github/instructions/dev/dev.instructions.md' }, + @{ kind = 'skill'; path = '.github/skills/video-to-gif/' } + ) + } + $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable', 'preview') $result.Agents | Should -Contain 'dev-agent' - $result.Agents | Should -Not -Contain 'all-agent' + $result.Prompts | Should -Contain 'dev-prompt' + $result.Instructions | Should -Contain 'dev/dev' + $result.Skills | Should -Contain 'video-to-gif' } - It 'Applies include patterns' { + It 'Uses item maturity when provided' { $collection = @{ - personas = @('developer') - include = @{ agents = @('dev-*') } + items = @( + @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md'; maturity = 'stable' }, + @{ kind = 'agent'; path = '.github/agents/preview-dev.agent.md'; maturity = 'preview' } + ) } - $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable', 'preview') + + $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable') $result.Agents | Should -Contain 'dev-agent' + $result.Agents | Should -Not -Contain 'preview-dev' } - It 'Applies exclude patterns' { + It 'Falls back to registry maturity when item maturity is omitted' { $collection = @{ - personas = @('developer') - exclude = @{ agents = @('preview-*') } + items = @( + @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md' }, + @{ kind = 'agent'; path = '.github/agents/preview-dev.agent.md' } + ) } - $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable', 'preview') + + $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable') $result.Agents | Should -Contain 'dev-agent' $result.Agents | Should -Not -Contain 'preview-dev' } - It 'Respects maturity filter' { - $collection = @{ personas = @('developer') } + It 'Returns empty when collection has no items' { + $collection = @{ id = 'empty' } $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable') - $result.Agents | Should -Contain 'dev-agent' - $result.Agents | Should -Not -Contain 'preview-dev' + $result.Agents.Count | Should -Be 0 + $result.Prompts.Count | Should -Be 0 + $result.Instructions.Count | Should -Be 0 + $result.Skills.Count | Should -Be 0 } } @@ -1240,7 +1259,7 @@ applyTo: "**/*.js" -DryRun $result.Success | Should -BeTrue - $result.AgentCount | Should -BeGreaterThan 0 + $result.ErrorMessage | Should -Be '' } } } diff --git a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 b/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 deleted file mode 100644 index 23f07a01..00000000 --- a/scripts/tests/linting/Validate-ArtifactRegistry.Tests.ps1 +++ /dev/null @@ -1,1631 +0,0 @@ -#Requires -Modules Pester -# Copyright (c) Microsoft Corporation. -# SPDX-License-Identifier: MIT - -BeforeAll { - # Dot-source the main script to make functions available - $scriptPath = Join-Path $PSScriptRoot '../../linting/Validate-ArtifactRegistry.ps1' - . $scriptPath - - # Import CI helpers module - $ciHelpersPath = Join-Path $PSScriptRoot '../../lib/Modules/CIHelpers.psm1' - Import-Module $ciHelpersPath -Force - - # Set up fixture paths - $script:FixtureDir = Join-Path $PSScriptRoot '../Fixtures/ArtifactRegistry' - $script:RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../../..')).Path - - # Fixture file paths - $script:ValidRegistryPath = Join-Path $script:FixtureDir 'valid-registry.json' - $script:InvalidJsonPath = Join-Path $script:FixtureDir 'invalid-json.json' - $script:MissingFieldsPath = Join-Path $script:FixtureDir 'missing-fields.json' - $script:InvalidVersionPath = Join-Path $script:FixtureDir 'invalid-version.json' - $script:MissingPersonasDefsPath = Join-Path $script:FixtureDir 'missing-personas-defs.json' - $script:InvalidPersonaIdPath = Join-Path $script:FixtureDir 'invalid-persona-id.json' - $script:MissingPersonaNamePath = Join-Path $script:FixtureDir 'missing-persona-name.json' - $script:UndefinedPersonaRefPath = Join-Path $script:FixtureDir 'undefined-persona-ref.json' - $script:UnknownDepRefsPath = Join-Path $script:FixtureDir 'unknown-dep-refs.json' - $script:CircularDepsPath = Join-Path $script:FixtureDir 'circular-deps.json' - $script:NoRequiresPath = Join-Path $script:FixtureDir 'no-requires.json' - $script:InvalidMaturityPath = Join-Path $script:FixtureDir 'invalid-maturity.json' - $script:MissingArtifactFieldsPath = Join-Path $script:FixtureDir 'missing-artifact-fields.json' - $script:ExtraPropertiesPath = Join-Path $script:FixtureDir 'extra-properties.json' - $script:InvalidPersonaFormatInArtifactPath = Join-Path $script:FixtureDir 'invalid-persona-format-in-artifact.json' - $script:ExtraPersonaPropertiesPath = Join-Path $script:FixtureDir 'extra-persona-properties.json' - $script:RenamedFileMismatchPath = Join-Path $script:FixtureDir 'renamed-file-mismatch.json' - $script:SchemaPath = Join-Path $PSScriptRoot '../../linting/schemas/ai-artifacts-registry.schema.json' -} - -#region Test-RegistryStructure Tests - -Describe 'Test-RegistryStructure' -Tag 'Unit' { - Context 'JSON parsing' { - It 'Returns error when JSON is malformed' { - $result = Test-RegistryStructure -RegistryPath $script:InvalidJsonPath - $result.Success | Should -BeFalse - $result.Errors[0] | Should -Match 'Failed to parse registry JSON' - $result.Registry | Should -BeNull - } - - It 'Parses valid JSON successfully' { - $result = Test-RegistryStructure -RegistryPath $script:ValidRegistryPath - $result.Success | Should -BeTrue - $result.Registry | Should -Not -BeNull - } - } - - Context 'Required fields validation' { - It 'Reports missing $schema field' { - $result = Test-RegistryStructure -RegistryPath $script:MissingFieldsPath - $result.Success | Should -BeFalse - $result.Errors | Should -Contain 'Missing required field: $schema' - } - - It 'Reports all missing required fields' { - $result = Test-RegistryStructure -RegistryPath $script:MissingFieldsPath - # Should report missing: $schema, version, personas, agents, prompts, instructions, skills - $result.Errors.Count | Should -BeGreaterOrEqual 6 - } - } - - Context 'Version format validation' { - It 'Reports invalid version format' { - $result = Test-RegistryStructure -RegistryPath $script:InvalidVersionPath - $result.Errors | Should -Contain 'Invalid version format: 1.0.0. Expected: major.minor' - } - - It 'Accepts valid version format' { - $result = Test-RegistryStructure -RegistryPath $script:ValidRegistryPath - $result.Errors | Where-Object { $_ -match 'version format' } | Should -BeNullOrEmpty - } - } - - Context 'Personas structure' { - It 'Reports missing personas.definitions' { - $result = Test-RegistryStructure -RegistryPath $script:MissingPersonasDefsPath - $result.Errors | Should -Contain 'Missing required field: personas.definitions' - } - } - - Context 'Additional properties validation' { - It 'Reports unexpected top-level properties' { - $result = Test-RegistryStructure -RegistryPath $script:ExtraPropertiesPath - $result.Errors | Where-Object { $_ -match 'Unexpected top-level property: extraTopLevel' } | Should -Not -BeNullOrEmpty - } - - It 'Reports unexpected personas sub-properties' { - $result = Test-RegistryStructure -RegistryPath $script:ExtraPersonaPropertiesPath - $result.Errors | Where-Object { $_ -match 'Unexpected property in personas: extraSection' } | Should -Not -BeNullOrEmpty - } - - It 'Does not report errors for valid top-level properties' { - $result = Test-RegistryStructure -RegistryPath $script:ValidRegistryPath - $result.Errors | Where-Object { $_ -match 'Unexpected' } | Should -BeNullOrEmpty - } - } -} - -#endregion - -#region Test-PersonaReferences Tests - -Describe 'Test-PersonaReferences' -Tag 'Unit' { - BeforeAll { - # Load valid registry for reference - $content = Get-Content $script:ValidRegistryPath -Raw - $script:ValidRegistry = $content | ConvertFrom-Json -AsHashtable - } - - Context 'Persona definition validation' { - It 'Reports invalid persona ID format' { - $content = Get-Content $script:InvalidPersonaIdPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-PersonaReferences -Registry $registry - $result.Errors | Where-Object { $_ -match 'Invalid persona ID format' } | Should -Not -BeNullOrEmpty - } - - It 'Reports missing name field' { - $content = Get-Content $script:MissingPersonaNamePath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-PersonaReferences -Registry $registry - $result.Errors | Where-Object { $_ -match "missing 'name' field" } | Should -Not -BeNullOrEmpty - } - - It 'Reports missing description field' { - # Create registry with persona missing description - $registry = @{ - personas = @{ - definitions = @{ - 'test-persona' = @{ name = 'Test' } - } - } - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Test-PersonaReferences -Registry $registry - $result.Errors | Where-Object { $_ -match "missing 'description' field" } | Should -Not -BeNullOrEmpty - } - - It 'Passes with valid persona definitions' { - $result = Test-PersonaReferences -Registry $script:ValidRegistry - $result.Success | Should -BeTrue - } - } - - Context 'Persona reference validation' { - It 'Reports undefined persona references in artifacts' { - $content = Get-Content $script:UndefinedPersonaRefPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-PersonaReferences -Registry $registry - $result.Errors | Where-Object { $_ -match 'references undefined persona' } | Should -Not -BeNullOrEmpty - } - } - - Context 'Persona definition additional properties' { - It 'Reports unexpected properties on persona definitions' { - $content = Get-Content $script:ExtraPersonaPropertiesPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-PersonaReferences -Registry $registry - $result.Errors | Where-Object { $_ -match "has unexpected property: extraField" } | Should -Not -BeNullOrEmpty - } - - It 'Does not report errors for valid persona definitions' { - $result = Test-PersonaReferences -Registry $script:ValidRegistry - $result.Errors | Where-Object { $_ -match 'unexpected property' } | Should -BeNullOrEmpty - } - } -} - -#endregion - -#region Test-ArtifactEntries Tests - -Describe 'Test-ArtifactEntries' -Tag 'Unit' { - BeforeAll { - $content = Get-Content $script:ValidRegistryPath -Raw - $script:ValidRegistry = $content | ConvertFrom-Json -AsHashtable - } - - Context 'Valid registry' { - It 'Passes with fully valid registry' { - $result = Test-ArtifactEntries -Registry $script:ValidRegistry - $result.Success | Should -BeTrue - $result.Errors.Count | Should -Be 0 - } - } - - Context 'Maturity validation' { - It 'Reports invalid maturity value' { - $content = Get-Content $script:InvalidMaturityPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match "invalid maturity 'INVALID'" } | Should -Not -BeNullOrEmpty - } - - It 'Accepts all valid maturity values' { - foreach ($maturity in @('stable', 'preview', 'experimental', 'deprecated')) { - $registry = @{ - agents = @{} - prompts = @{ - 'test' = @{ maturity = $maturity; personas = @(); tags = @() } - } - instructions = @{} - skills = @{} - } - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match 'maturity' } | Should -BeNullOrEmpty - } - } - } - - Context 'Required artifact fields' { - It 'Reports missing maturity field' { - $content = Get-Content $script:MissingArtifactFieldsPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match "missing required field 'maturity'" } | Should -Not -BeNullOrEmpty - } - - It 'Reports missing personas field' { - $content = Get-Content $script:MissingArtifactFieldsPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match "missing required field 'personas'" } | Should -Not -BeNullOrEmpty - } - - It 'Reports missing tags field' { - $registry = @{ - agents = @{} - prompts = @{ - 'no-tags' = @{ maturity = 'stable'; personas = @() } - } - instructions = @{} - skills = @{} - } - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match "missing required field 'tags'" } | Should -Not -BeNullOrEmpty - } - } - - Context 'Persona reference format in artifacts' { - It 'Reports invalid persona reference format' { - $content = Get-Content $script:InvalidPersonaFormatInArtifactPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match "invalid persona reference format 'Bad Format!'" } | Should -Not -BeNullOrEmpty - $result.Errors | Where-Object { $_ -match "invalid persona reference format '123-invalid'" } | Should -Not -BeNullOrEmpty - } - - It 'Accepts valid persona reference format' { - $registry = @{ - agents = @{} - prompts = @{ - 'good' = @{ maturity = 'stable'; personas = @('developer', 'dev-ops-2'); tags = @() } - } - instructions = @{} - skills = @{} - } - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match 'persona reference format' } | Should -BeNullOrEmpty - } - } - - Context 'Additional properties on artifacts' { - It 'Reports unexpected properties on agent entries' { - $content = Get-Content $script:ExtraPropertiesPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match "unexpected property 'customField'" } | Should -Not -BeNullOrEmpty - } - - It 'Reports requires block on non-agent sections' { - $content = Get-Content $script:ExtraPropertiesPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match "prompts/extra-prompt.*unexpected property 'requires'" } | Should -Not -BeNullOrEmpty - } - - It 'Allows requires block on agent entries' { - $result = Test-ArtifactEntries -Registry $script:ValidRegistry - $result.Errors | Where-Object { $_ -match "unexpected property 'requires'" } | Should -BeNullOrEmpty - } - } - - Context 'Agent requires block structure' { - It 'Reports unexpected properties in requires block' { - $registry = @{ - agents = @{ - 'bad-requires' = @{ - maturity = 'stable' - personas = @() - tags = @() - requires = @{ agents = @(); unknownRef = @() } - } - } - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match "unexpected property in requires: 'unknownRef'" } | Should -Not -BeNullOrEmpty - } - - It 'Reports non-array values in requires block' { - $registry = @{ - agents = @{ - 'bad-requires' = @{ - maturity = 'stable' - personas = @() - tags = @() - requires = @{ agents = 'not-an-array' } - } - } - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match 'requires.agents must be an array' } | Should -Not -BeNullOrEmpty - } - } - - Context 'Type validation' { - It 'Reports non-array personas value' { - $registry = @{ - agents = @{} - prompts = @{ - 'bad-type' = @{ maturity = 'stable'; personas = 'not-array'; tags = @() } - } - instructions = @{} - skills = @{} - } - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match "'personas' must be an array" } | Should -Not -BeNullOrEmpty - } - - It 'Reports non-array tags value' { - $registry = @{ - agents = @{} - prompts = @{ - 'bad-type' = @{ maturity = 'stable'; personas = @(); tags = 'not-array' } - } - instructions = @{} - skills = @{} - } - $result = Test-ArtifactEntries -Registry $registry - $result.Errors | Where-Object { $_ -match "'tags' must be an array" } | Should -Not -BeNullOrEmpty - } - } -} - -#endregion - -#region Test-JsonSchemaValidation Tests - -Describe 'Test-JsonSchemaValidation' -Tag 'Unit' { - Context 'Schema file existence' { - It 'Reports error when schema file does not exist' { - $result = Test-JsonSchemaValidation -RegistryPath $script:ValidRegistryPath -SchemaPath '/nonexistent/schema.json' - $result.Success | Should -BeFalse - $result.Errors[0] | Should -Match 'Schema file not found' - } - } - - Context 'Valid registry against schema' { - It 'Passes with valid registry' { - $result = Test-JsonSchemaValidation -RegistryPath $script:ValidRegistryPath -SchemaPath $script:SchemaPath - $result.Success | Should -BeTrue - $result.Errors.Count | Should -Be 0 - } - } - - Context 'Invalid registry against schema' { - It 'Reports schema violations for invalid maturity' { - $result = Test-JsonSchemaValidation -RegistryPath $script:InvalidMaturityPath -SchemaPath $script:SchemaPath - $result.Success | Should -BeFalse - $result.Errors.Count | Should -BeGreaterThan 0 - } - - It 'Reports schema violations for missing required artifact fields' { - $result = Test-JsonSchemaValidation -RegistryPath $script:MissingArtifactFieldsPath -SchemaPath $script:SchemaPath - $result.Success | Should -BeFalse - $result.Errors.Count | Should -BeGreaterThan 0 - } - - It 'Reports schema violations for extra properties' { - $result = Test-JsonSchemaValidation -RegistryPath $script:ExtraPropertiesPath -SchemaPath $script:SchemaPath - $result.Success | Should -BeFalse - $result.Errors.Count | Should -BeGreaterThan 0 - } - } -} - -#endregion - -#region Get-ArtifactPath Tests - -Describe 'Get-ArtifactPath' -Tag 'Unit' { - BeforeAll { - $script:TestRepoRoot = '/test/repo' - } - - Context 'Section path mapping' { - It 'Returns correct path for agents section' { - $result = Get-ArtifactPath -Section 'agents' -Key 'test-agent' -RepoRoot $script:TestRepoRoot - $result | Should -Be '/test/repo/.github/agents/test-agent.agent.md' - } - - It 'Returns correct path for prompts section' { - $result = Get-ArtifactPath -Section 'prompts' -Key 'test-prompt' -RepoRoot $script:TestRepoRoot - $result | Should -Be '/test/repo/.github/prompts/test-prompt.prompt.md' - } - - It 'Returns correct path for instructions section' { - $result = Get-ArtifactPath -Section 'instructions' -Key 'test-instruction' -RepoRoot $script:TestRepoRoot - $result | Should -Be '/test/repo/.github/instructions/test-instruction.instructions.md' - } - - It 'Returns correct path for skills section' { - $result = Get-ArtifactPath -Section 'skills' -Key 'test-skill' -RepoRoot $script:TestRepoRoot - $result | Should -Be '/test/repo/.github/skills/test-skill/SKILL.md' - } - } - - Context 'Unknown section handling' { - It 'Returns null for unknown section' { - $result = Get-ArtifactPath -Section 'unknown' -Key 'test' -RepoRoot $script:TestRepoRoot - $result | Should -BeNull - } - } -} - -#endregion - -#region Test-ArtifactFileExistence Tests - -Describe 'Test-ArtifactFileExistence' -Tag 'Unit' { - Context 'File existence checks' { - It 'Returns success when all files exist' { - Mock Test-Path { return $true } - $registry = @{ - agents = @{ 'existing-agent' = @{} } - prompts = @{ 'existing-prompt' = @{} } - instructions = @{ 'existing-instruction' = @{} } - skills = @{ 'existing-skill' = @{} } - } - $result = Test-ArtifactFileExistence -Registry $registry -RepoRoot $TestDrive - $result.Success | Should -BeTrue - $result.Errors.Count | Should -Be 0 - } - - It 'Returns error for missing file' { - Mock Test-Path { return $false } - $registry = @{ - agents = @{ 'missing-agent' = @{} } - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Test-ArtifactFileExistence -Registry $registry -RepoRoot $TestDrive - $result.Success | Should -BeFalse - $result.Errors[0] | Should -Match 'agents/missing-agent: File not found' - } - } -} - -#endregion - -#region Test-DependencyReferences Tests - -Describe 'Test-DependencyReferences' -Tag 'Unit' { - BeforeAll { - $script:BaseRegistry = @{ - agents = @{ - 'agent-a' = @{ - requires = @{ - agents = @() - prompts = @() - instructions = @() - skills = @() - } - } - } - prompts = @{ 'prompt-a' = @{} } - instructions = @{ 'instruction-a' = @{} } - skills = @{ 'skill-a' = @{} } - } - } - - Context 'Dependency validation' { - It 'Reports unknown agent reference' { - $content = Get-Content $script:UnknownDepRefsPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-DependencyReferences -Registry $registry - $result.Errors | Where-Object { $_ -match 'requires.agents references unknown agent' } | Should -Not -BeNullOrEmpty - } - - It 'Reports unknown prompt reference' { - $content = Get-Content $script:UnknownDepRefsPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-DependencyReferences -Registry $registry - $result.Errors | Where-Object { $_ -match 'requires.prompts references unknown prompt' } | Should -Not -BeNullOrEmpty - } - - It 'Reports unknown instruction reference' { - $content = Get-Content $script:UnknownDepRefsPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-DependencyReferences -Registry $registry - $result.Errors | Where-Object { $_ -match 'requires.instructions references unknown instruction' } | Should -Not -BeNullOrEmpty - } - - It 'Reports unknown skill reference' { - $content = Get-Content $script:UnknownDepRefsPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-DependencyReferences -Registry $registry - $result.Errors | Where-Object { $_ -match 'requires.skills references unknown skill' } | Should -Not -BeNullOrEmpty - } - - It 'Passes with valid references' { - $registry = @{ - agents = @{ - 'agent-a' = @{ - requires = @{ - agents = @() - prompts = @('prompt-a') - instructions = @() - skills = @() - } - } - } - prompts = @{ 'prompt-a' = @{} } - instructions = @{} - skills = @{} - } - $result = Test-DependencyReferences -Registry $registry - $result.Success | Should -BeTrue - } - - It 'Skips agents without requires block' { - $content = Get-Content $script:NoRequiresPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-DependencyReferences -Registry $registry - $result.Success | Should -BeTrue - } - } - - Context 'Circular dependency detection' { - It 'Returns warnings for circular dependencies' { - $content = Get-Content $script:CircularDepsPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-DependencyReferences -Registry $registry - $result.Warnings.Count | Should -BeGreaterThan 0 - $result.Warnings[0] | Should -Match 'Circular agent dependency detected' - } - - It 'Success remains true with circular warnings' { - $content = Get-Content $script:CircularDepsPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-DependencyReferences -Registry $registry - # Circular deps are warnings, not errors - $result.Success | Should -BeTrue - } - } -} - -#endregion - -#region Find-CircularAgentDependencies Tests - -Describe 'Find-CircularAgentDependencies' -Tag 'Unit' { - Context 'Cycle detection' { - It 'Returns empty list when no cycles exist' { - $registry = @{ - agents = @{ - 'agent-a' = @{ requires = @{ agents = @('agent-b') } } - 'agent-b' = @{ requires = @{ agents = @() } } - } - } - $result = Find-CircularAgentDependencies -Registry $registry - $result.Count | Should -Be 0 - } - - It 'Detects simple A -> B -> A cycle' { - $registry = @{ - agents = @{ - 'agent-a' = @{ requires = @{ agents = @('agent-b') } } - 'agent-b' = @{ requires = @{ agents = @('agent-a') } } - } - } - $result = Find-CircularAgentDependencies -Registry $registry - $result.Count | Should -BeGreaterThan 0 - } - - It 'Detects A -> B -> C -> A cycle' { - $content = Get-Content $script:CircularDepsPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Find-CircularAgentDependencies -Registry $registry - $result.Count | Should -BeGreaterThan 0 - } - - It 'Does not report non-cyclic paths' { - $registry = @{ - agents = @{ - 'agent-a' = @{ requires = @{ agents = @('agent-b', 'agent-c') } } - 'agent-b' = @{ requires = @{ agents = @('agent-d') } } - 'agent-c' = @{ requires = @{ agents = @('agent-d') } } - 'agent-d' = @{ requires = @{ agents = @() } } - } - } - $result = Find-CircularAgentDependencies -Registry $registry - $result.Count | Should -Be 0 - } - } -} - -#endregion - -#region Find-CycleFromAgent Tests - -Describe 'Find-CycleFromAgent' -Tag 'Unit' { - Context 'Recursive cycle detection' { - It 'Handles agent with no requires.agents' { - $registry = @{ - agents = @{ - 'agent-a' = @{ requires = @{ prompts = @() } } # no agents key - } - } - $result = Find-CircularAgentDependencies -Registry $registry - $result.Count | Should -Be 0 - } - - It 'Skips references to nonexistent agents' { - $registry = @{ - agents = @{ - 'agent-a' = @{ requires = @{ agents = @('nonexistent') } } - } - } - $result = Find-CircularAgentDependencies -Registry $registry - $result.Count | Should -Be 0 - } - - It 'Handles self-referencing agent' { - $registry = @{ - agents = @{ - 'agent-a' = @{ requires = @{ agents = @('agent-a') } } - } - } - $result = Find-CircularAgentDependencies -Registry $registry - $result.Count | Should -BeGreaterThan 0 - } - - It 'Deduplicates equivalent cycles using global visited tracking' { - $registry = @{ - agents = @{ - 'agent-a' = @{ requires = @{ agents = @('agent-b') } } - 'agent-b' = @{ requires = @{ agents = @('agent-a') } } - } - } - $result = Find-CircularAgentDependencies -Registry $registry - # The function may report cycles from different starting points - # but the global visited hash prevents truly identical cycles - $result.Count | Should -BeGreaterThan 0 - # Verify cycles are detected - ($result | ForEach-Object { $_ -join ',' }) | Should -Match 'agent-a|agent-b' - } - - It 'Detects multiple independent cycles' { - $registry = @{ - agents = @{ - 'agent-a' = @{ requires = @{ agents = @('agent-b') } } - 'agent-b' = @{ requires = @{ agents = @('agent-a') } } - 'agent-c' = @{ requires = @{ agents = @('agent-d') } } - 'agent-d' = @{ requires = @{ agents = @('agent-c') } } - } - } - $result = Find-CircularAgentDependencies -Registry $registry - $result.Count | Should -BeGreaterOrEqual 2 - } - } -} - -#endregion - -#region Find-OrphanArtifacts Tests - -Describe 'Find-OrphanArtifacts' -Tag 'Unit' { - BeforeAll { - $script:OrphanTestRoot = Join-Path $TestDrive 'orphan-test-repo' - } - - BeforeEach { - # Clean and recreate test directory structure - if (Test-Path $script:OrphanTestRoot) { - Remove-Item -Path $script:OrphanTestRoot -Recurse -Force - } - New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/agents" -Force | Out-Null - New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/prompts" -Force | Out-Null - New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/instructions" -Force | Out-Null - New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/skills" -Force | Out-Null - } - - Context 'Orphan detection by section' { - It 'Returns empty warnings when no orphans exist' { - $registry = @{ - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot - $result.Warnings.Count | Should -Be 0 - } - - It 'Detects orphan agent file' { - Set-Content -Path "$script:OrphanTestRoot/.github/agents/orphan-agent.agent.md" -Value '# Orphan' - $registry = @{ - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot - $result.Warnings | Where-Object { $_ -match 'Orphan agent file' } | Should -Not -BeNullOrEmpty - } - - It 'Detects orphan prompt file' { - Set-Content -Path "$script:OrphanTestRoot/.github/prompts/orphan-prompt.prompt.md" -Value '# Orphan' - $registry = @{ - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot - $result.Warnings | Where-Object { $_ -match 'Orphan prompt file' } | Should -Not -BeNullOrEmpty - } - - It 'Detects orphan instruction file in subdirectory' { - New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/instructions/subdir" -Force | Out-Null - Set-Content -Path "$script:OrphanTestRoot/.github/instructions/subdir/orphan.instructions.md" -Value '# Orphan' - $registry = @{ - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot - $result.Warnings | Where-Object { $_ -match 'Orphan instruction file' } | Should -Not -BeNullOrEmpty - } - - It 'Detects orphan skill directory' { - New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/skills/orphan-skill" -Force | Out-Null - Set-Content -Path "$script:OrphanTestRoot/.github/skills/orphan-skill/SKILL.md" -Value '# Orphan Skill' - $registry = @{ - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot - $result.Warnings | Where-Object { $_ -match 'Orphan skill directory' } | Should -Not -BeNullOrEmpty - } - } - - Context 'Repo-specific instruction exclusion' { - It 'Skips instruction files in hve-core subdirectory' { - New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/instructions/hve-core" -Force | Out-Null - Set-Content -Path "$script:OrphanTestRoot/.github/instructions/hve-core/workflows.instructions.md" -Value '# Repo-specific' - $registry = @{ - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot - $result.Warnings | Where-Object { $_ -match 'hve-core' } | Should -BeNullOrEmpty - } - - It 'Still detects orphan instructions in other subdirectories' { - New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/instructions/hve-core" -Force | Out-Null - New-Item -ItemType Directory -Path "$script:OrphanTestRoot/.github/instructions/other" -Force | Out-Null - Set-Content -Path "$script:OrphanTestRoot/.github/instructions/hve-core/workflows.instructions.md" -Value '# Repo-specific' - Set-Content -Path "$script:OrphanTestRoot/.github/instructions/other/orphan.instructions.md" -Value '# Orphan' - $registry = @{ - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:OrphanTestRoot - $result.Warnings | Where-Object { $_ -match 'other/orphan' } | Should -Not -BeNullOrEmpty - $result.Warnings | Where-Object { $_ -match 'hve-core' } | Should -BeNullOrEmpty - } - } - - Context 'Missing directories' { - It 'Handles missing artifact directories gracefully' { - $emptyRepoRoot = Join-Path $TestDrive 'empty-repo' - New-Item -ItemType Directory -Path $emptyRepoRoot -Force | Out-Null - - $registry = @{ - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Find-OrphanArtifacts -Registry $registry -RepoRoot $emptyRepoRoot - $result.Warnings.Count | Should -Be 0 - } - } -} - -#endregion - -#region Renamed-File Mismatch Tests (Dual-Direction Detection) - -Describe 'Renamed-File Mismatch Detection' -Tag 'Unit' { - BeforeAll { - $script:RenameTestRoot = Join-Path $TestDrive 'rename-test-repo' - } - - BeforeEach { - if (Test-Path $script:RenameTestRoot) { - Remove-Item -Path $script:RenameTestRoot -Recurse -Force - } - New-Item -ItemType Directory -Path "$script:RenameTestRoot/.github/agents" -Force | Out-Null - New-Item -ItemType Directory -Path "$script:RenameTestRoot/.github/prompts" -Force | Out-Null - New-Item -ItemType Directory -Path "$script:RenameTestRoot/.github/instructions" -Force | Out-Null - New-Item -ItemType Directory -Path "$script:RenameTestRoot/.github/skills" -Force | Out-Null - } - - Context 'File added but not in registry (orphan promoted to error with -WarningsAsErrors)' { - It 'Detects orphan file and treats as error when WarningsAsErrors is enabled' { - Set-Content -Path "$script:RenameTestRoot/.github/agents/new-unregistered.agent.md" -Value '# New Agent' - $registry = @{ - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - $orphanResult = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:RenameTestRoot - $orphanResult.Warnings | Where-Object { $_ -match 'new-unregistered' } | Should -Not -BeNullOrEmpty - } - } - - Context 'File removed but still in registry (existence error)' { - It 'Detects missing file for stale registry key' { - # Registry references old-agent-name but no file exists - $content = Get-Content $script:RenamedFileMismatchPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - $result = Test-ArtifactFileExistence -Registry $registry -RepoRoot $script:RenameTestRoot - $result.Success | Should -BeFalse - $result.Errors | Where-Object { $_ -match 'agents/old-agent-name: File not found' } | Should -Not -BeNullOrEmpty - } - } - - Context 'Renamed file: old key errors AND new file detected as orphan' { - It 'Detects both old-key existence error and new-file orphan warning' { - # Registry has old-agent-name; file was renamed to new-agent-name on disk - Set-Content -Path "$script:RenameTestRoot/.github/agents/new-agent-name.agent.md" -Value '# Renamed Agent' - - $content = Get-Content $script:RenamedFileMismatchPath -Raw - $registry = $content | ConvertFrom-Json -AsHashtable - - # Old key should fail existence check - $existenceResult = Test-ArtifactFileExistence -Registry $registry -RepoRoot $script:RenameTestRoot - $existenceResult.Success | Should -BeFalse - $existenceResult.Errors | Where-Object { $_ -match 'agents/old-agent-name: File not found' } | Should -Not -BeNullOrEmpty - - # New file should be detected as orphan - $orphanResult = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:RenameTestRoot - $orphanResult.Warnings | Where-Object { $_ -match 'new-agent-name' } | Should -Not -BeNullOrEmpty - } - - It 'Covers instruction file rename across subdirectories' { - # Registry has instruction key "subdir/old-instruction"; file renamed to "subdir/new-instruction" - New-Item -ItemType Directory -Path "$script:RenameTestRoot/.github/instructions/subdir" -Force | Out-Null - Set-Content -Path "$script:RenameTestRoot/.github/instructions/subdir/new-instruction.instructions.md" -Value '# Renamed Instruction' - - $registry = @{ - agents = @{} - prompts = @{} - instructions = @{ - 'subdir/old-instruction' = @{ - maturity = 'stable' - personas = @('developer') - tags = @('test') - } - } - skills = @{} - } - - # Old key should fail existence check - $existenceResult = Test-ArtifactFileExistence -Registry $registry -RepoRoot $script:RenameTestRoot - $existenceResult.Success | Should -BeFalse - $existenceResult.Errors | Where-Object { $_ -match 'instructions/subdir/old-instruction: File not found' } | Should -Not -BeNullOrEmpty - - # New file should be detected as orphan - $orphanResult = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:RenameTestRoot - $orphanResult.Warnings | Where-Object { $_ -match 'new-instruction' } | Should -Not -BeNullOrEmpty - } - } - - Context 'hve-core exclusion preserved during rename scenarios' { - It 'Does not report orphan for renamed file under hve-core directory' { - New-Item -ItemType Directory -Path "$script:RenameTestRoot/.github/instructions/hve-core" -Force | Out-Null - Set-Content -Path "$script:RenameTestRoot/.github/instructions/hve-core/renamed-workflow.instructions.md" -Value '# Renamed' - - $registry = @{ - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - - $orphanResult = Find-OrphanArtifacts -Registry $registry -RepoRoot $script:RenameTestRoot - $orphanResult.Warnings | Where-Object { $_ -match 'hve-core' } | Should -BeNullOrEmpty - } - } -} - -#endregion - -#region Test-CollectionManifests Tests - -Describe 'Test-CollectionManifests' -Tag 'Unit' { - BeforeAll { - $script:TestRoot = Join-Path $TestDrive 'collection-manifests-test' - $script:CollectionsDir = Join-Path $script:TestRoot 'extension/collections' - $script:SchemaDir = Join-Path $script:TestRoot 'scripts/linting/schemas' - - $script:BaseRegistry = @{ - personas = @{ - definitions = @{ - 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } - 'developer' = @{ name = 'Developer'; description = 'Developer artifacts' } - } - } - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - } - - BeforeEach { - if (Test-Path $script:TestRoot) { - Remove-Item -Path $script:TestRoot -Recurse -Force - } - New-Item -ItemType Directory -Path $script:CollectionsDir -Force | Out-Null - New-Item -ItemType Directory -Path $script:SchemaDir -Force | Out-Null - - # Copy real schema for JSON Schema validation - $realSchemaPath = Join-Path $PSScriptRoot '../../linting/schemas/collection.schema.json' - if (Test-Path $realSchemaPath) { - Copy-Item -Path $realSchemaPath -Destination (Join-Path $script:SchemaDir 'collection.schema.json') - } - } - - Context 'Missing collections directory' { - It 'Returns success when collections directory does not exist' { - $emptyRoot = Join-Path $TestDrive 'no-collections' - New-Item -ItemType Directory -Path $emptyRoot -Force | Out-Null - - $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $emptyRoot - $result.Success | Should -BeTrue - $result.Errors.Count | Should -Be 0 - $result.Warnings.Count | Should -Be 0 - } - } - - Context 'Valid manifests' { - It 'Passes for a valid collection manifest with stable maturity' { - @{ - '$schema' = '../schemas/collection.schema.json' - id = 'test-coll' - name = 'test-ext' - displayName = 'Test Collection' - description = 'A valid test collection' - personas = @('hve-core-all') - maturity = 'stable' - } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'test-coll.collection.json') - - $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot - $result.Success | Should -BeTrue - $result.Errors.Count | Should -Be 0 - } - - It 'Passes for manifest without maturity field (defaults assumed)' { - @{ - '$schema' = '../schemas/collection.schema.json' - id = 'no-maturity' - name = 'no-maturity-ext' - displayName = 'No Maturity' - description = 'Collection without maturity field' - personas = @('developer') - } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'no-maturity.collection.json') - - $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot - $result.Success | Should -BeTrue - } - } - - Context 'Invalid maturity values' { - It 'Reports error for invalid maturity enum value' { - @{ - '$schema' = '../schemas/collection.schema.json' - id = 'bad-maturity' - name = 'bad-maturity-ext' - displayName = 'Bad Maturity' - description = 'Collection with invalid maturity' - personas = @('hve-core-all') - maturity = 'alpha' - } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'bad-maturity.collection.json') - - $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot - $result.Success | Should -BeFalse - $result.Errors | Where-Object { $_ -match "invalid maturity 'alpha'" } | Should -Not -BeNullOrEmpty - } - } - - Context 'Deprecated collection warnings' { - It 'Emits warning for deprecated collection' { - @{ - '$schema' = '../schemas/collection.schema.json' - id = 'old-coll' - name = 'old-ext' - displayName = 'Old Collection' - description = 'A deprecated collection' - personas = @('hve-core-all') - maturity = 'deprecated' - } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'old-coll.collection.json') - - $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot - $result.Success | Should -BeTrue - $result.Warnings | Where-Object { $_ -match 'deprecated.*excluded from all builds' } | Should -Not -BeNullOrEmpty - } - } - - Context 'Persona reference validation' { - It 'Warns when persona is not defined in registry' { - @{ - '$schema' = '../schemas/collection.schema.json' - id = 'unknown-persona' - name = 'unknown-persona-ext' - displayName = 'Unknown Persona' - description = 'Collection with undefined persona' - personas = @('nonexistent-persona') - maturity = 'stable' - } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'unknown-persona.collection.json') - - $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot - $result.Warnings | Where-Object { $_ -match "references persona 'nonexistent-persona' not defined" } | Should -Not -BeNullOrEmpty - } - - It 'Does not warn for hve-core-all persona even when not in definitions' { - $emptyPersonaRegistry = @{ - personas = @{ definitions = @{} } - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - @{ - '$schema' = '../schemas/collection.schema.json' - id = 'all-coll' - name = 'all-ext' - displayName = 'All Collection' - description = 'Uses hve-core-all persona' - personas = @('hve-core-all') - maturity = 'stable' - } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'all-coll.collection.json') - - $result = Test-CollectionManifests -Registry $emptyPersonaRegistry -RepoRoot $script:TestRoot - $result.Warnings | Where-Object { $_ -match 'hve-core-all' } | Should -BeNullOrEmpty - } - } - - Context 'Invalid JSON' { - It 'Reports error for malformed JSON file' { - '{ invalid json }' | Set-Content -Path (Join-Path $script:CollectionsDir 'broken.collection.json') - - $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot - $result.Success | Should -BeFalse - $result.Errors | Where-Object { $_ -match 'Failed to parse JSON' } | Should -Not -BeNullOrEmpty - } - } - - Context 'Multiple manifest validation' { - It 'Validates all collection files in directory' { - # Valid manifest - @{ - '$schema' = '../schemas/collection.schema.json' - id = 'valid' - name = 'valid-ext' - displayName = 'Valid' - description = 'Valid collection' - personas = @('hve-core-all') - maturity = 'stable' - } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'valid.collection.json') - - # Invalid maturity manifest - @{ - '$schema' = '../schemas/collection.schema.json' - id = 'invalid' - name = 'invalid-ext' - displayName = 'Invalid' - description = 'Invalid collection' - personas = @('hve-core-all') - maturity = 'bogus' - } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir 'invalid.collection.json') - - $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot - $result.Success | Should -BeFalse - $result.Errors | Where-Object { $_ -match "invalid maturity 'bogus'" } | Should -Not -BeNullOrEmpty - } - } - - Context 'All valid maturity values accepted' { - It 'Accepts stable, preview, experimental, and deprecated maturities' { - foreach ($maturity in @('stable', 'preview', 'experimental', 'deprecated')) { - @{ - '$schema' = '../schemas/collection.schema.json' - id = "$maturity-coll" - name = "$maturity-ext" - displayName = "$maturity Collection" - description = "Collection with $maturity maturity" - personas = @('hve-core-all') - maturity = $maturity - } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:CollectionsDir "$maturity.collection.json") - } - - $result = Test-CollectionManifests -Registry $script:BaseRegistry -RepoRoot $script:TestRoot - # No maturity-related errors (deprecated warnings are fine) - $result.Errors | Where-Object { $_ -match 'invalid maturity' } | Should -BeNullOrEmpty - } - } -} - -#endregion - -#region Write-RegistryValidationOutput Tests - -Describe 'Write-RegistryValidationOutput' -Tag 'Unit' { - Context 'Console output formatting' { - It 'Outputs errors section when errors exist' { - $result = @{ - Errors = @('Error 1', 'Error 2') - Warnings = @() - ArtifactCounts = $null - } - - # Capture Write-Host output using 6>&1 - $output = Write-RegistryValidationOutput -Result $result -RegistryPath '/test/registry.json' 6>&1 - $outputText = $output -join "`n" - $outputText | Should -Match 'Errors \(2\)' - } - - It 'Outputs warnings section when warnings exist' { - $result = @{ - Errors = @() - Warnings = @('Warning 1') - ArtifactCounts = $null - } - - $output = Write-RegistryValidationOutput -Result $result -RegistryPath '/test/registry.json' 6>&1 - $outputText = $output -join "`n" - $outputText | Should -Match 'Warnings \(1\)' - } - - It 'Outputs clean summary without errors or warnings' { - $result = @{ - Errors = @() - Warnings = @() - ArtifactCounts = $null - } - - $output = Write-RegistryValidationOutput -Result $result -RegistryPath '/test/registry.json' 6>&1 - $outputText = $output -join "`n" - $outputText | Should -Match 'Errors: 0' - } - - It 'Outputs artifact counts when provided' { - $result = @{ - Errors = @() - Warnings = @() - ArtifactCounts = @{ - Agents = 10 - Prompts = 5 - Instructions = 8 - Skills = 2 - } - } - - $output = Write-RegistryValidationOutput -Result $result -RegistryPath '/test/registry.json' 6>&1 - $outputText = $output -join "`n" - $outputText | Should -Match 'Agents: 10' - $outputText | Should -Match 'Prompts: 5' - $outputText | Should -Match 'Instructions: 8' - $outputText | Should -Match 'Skills: 2' - } - } -} - -#endregion - -#region Export-RegistryValidationResults Tests - -Describe 'Export-RegistryValidationResults' -Tag 'Unit' { - Context 'JSON export' { - It 'Creates output directory if missing' { - $outputPath = Join-Path $TestDrive 'new-dir/results.json' - $result = @{ - Success = $true - Errors = @() - Warnings = @() - ArtifactCounts = $null - } - - Export-RegistryValidationResults -Result $result -OutputPath $outputPath - - Test-Path (Split-Path $outputPath -Parent) | Should -BeTrue - } - - It 'Uses existing output directory' { - $existingDir = Join-Path $TestDrive 'existing-dir' - New-Item -ItemType Directory -Path $existingDir -Force | Out-Null - $outputPath = Join-Path $existingDir 'results.json' - $result = @{ - Success = $true - Errors = @() - Warnings = @() - ArtifactCounts = $null - } - - Export-RegistryValidationResults -Result $result -OutputPath $outputPath - - Test-Path $outputPath | Should -BeTrue - } - - It 'Writes correct JSON structure' { - $outputPath = Join-Path $TestDrive 'structure-test.json' - $result = @{ - Success = $true - Errors = @('error1') - Warnings = @('warning1') - ArtifactCounts = @{ Agents = 5 } - } - - Export-RegistryValidationResults -Result $result -OutputPath $outputPath - - $exported = Get-Content $outputPath -Raw | ConvertFrom-Json - $exported.success | Should -BeTrue - $exported.errors | Should -Contain 'error1' - $exported.warnings | Should -Contain 'warning1' - $exported.timestamp | Should -Not -BeNullOrEmpty - $exported.artifactCounts.Agents | Should -Be 5 - } - } -} - -#endregion - -#region Main Execution Block Tests - -Describe 'Main Execution Block' -Tag 'Integration' { - BeforeAll { - # Save original environment - $script:OriginalGHA = $env:GITHUB_ACTIONS - $script:OriginalTFBuild = $env:TF_BUILD - $script:MainScriptPath = Join-Path $PSScriptRoot '../../linting/Validate-ArtifactRegistry.ps1' - } - - AfterAll { - # Restore original environment - if ($null -eq $script:OriginalGHA) { - Remove-Item Env:GITHUB_ACTIONS -ErrorAction SilentlyContinue - } - else { - $env:GITHUB_ACTIONS = $script:OriginalGHA - } - if ($null -eq $script:OriginalTFBuild) { - Remove-Item Env:TF_BUILD -ErrorAction SilentlyContinue - } - else { - $env:TF_BUILD = $script:OriginalTFBuild - } - } - - BeforeEach { - # Reset environment - Remove-Item Env:GITHUB_ACTIONS -ErrorAction SilentlyContinue - Remove-Item Env:TF_BUILD -ErrorAction SilentlyContinue - } - - Context 'Repo root resolution' { - It 'Uses provided RepoRoot parameter' { - # Create minimal test repo structure - $testRepo = Join-Path $TestDrive 'test-repo' - New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null - Copy-Item -Path $script:ValidRegistryPath -Destination "$testRepo/.github/ai-artifacts-registry.json" - - # Mock Test-Path to simulate files exist - Mock Test-Path { return $true } -ParameterFilter { $Path -notlike '*results.json*' } - - # The script should not error when running with valid RepoRoot - { & $script:MainScriptPath -RepoRoot $testRepo -RegistryPath "$testRepo/.github/ai-artifacts-registry.json" -OutputPath "$testRepo/logs/results.json" 2>$null } | Should -Not -Throw - } - - It 'Derives RepoRoot from PSScriptRoot grandparent when not provided' { - # Run from the actual repo - without RepoRoot the script resolves $PSScriptRoot/../.. - $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -OutputPath '$TestDrive/results.json'; exit `$LASTEXITCODE" 2>&1 - # Script should resolve RepoRoot and produce output - Test-Path "$TestDrive/results.json" | Should -BeTrue - } - - - } - - Context 'Validation orchestration' { - It 'Reports error when registry file not found' { - $testRepo = Join-Path $TestDrive 'no-registry-repo' - New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null - - $output = & $script:MainScriptPath -RepoRoot $testRepo -RegistryPath "$testRepo/.github/nonexistent.json" -OutputPath "$testRepo/logs/results.json" 2>&1 - $output | Should -Match 'Registry file not found' - } - - It 'Stops validation early on structure failure' { - $testRepo = Join-Path $TestDrive 'invalid-structure-repo' - New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null - Copy-Item -Path $script:InvalidJsonPath -Destination "$testRepo/.github/ai-artifacts-registry.json" - - # Run directly with output suppression - exit code capture via $LASTEXITCODE - $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo' -OutputPath '$testRepo/logs/results.json'" 2>&1 - $LASTEXITCODE | Should -Be 1 - } - } - - Context 'CI environment handling' { - It 'Writes CI annotations when in GitHub Actions' { - $env:GITHUB_ACTIONS = 'true' - $testRepo = Join-Path $TestDrive 'gha-repo' - New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null - Copy-Item -Path $script:InvalidVersionPath -Destination "$testRepo/.github/ai-artifacts-registry.json" - - $output = & $script:MainScriptPath -RepoRoot $testRepo -OutputPath "$testRepo/logs/results.json" 2>&1 | Out-String - $output | Should -Match '::error' - } - - It 'Does not write CI annotations when not in CI' { - Remove-Item Env:GITHUB_ACTIONS -ErrorAction SilentlyContinue - Remove-Item Env:TF_BUILD -ErrorAction SilentlyContinue - $testRepo = Join-Path $TestDrive 'local-repo' - New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null - Copy-Item -Path $script:InvalidVersionPath -Destination "$testRepo/.github/ai-artifacts-registry.json" - - $output = & $script:MainScriptPath -RepoRoot $testRepo -OutputPath "$testRepo/logs/results.json" 2>&1 | Out-String - $output | Should -Not -Match '::error' - } - - It 'Writes CI warning annotations for orphan files in GitHub Actions' { - $env:GITHUB_ACTIONS = 'true' - $testRepo = Join-Path $TestDrive 'gha-warnings-repo' - New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/agents" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/prompts" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/instructions" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/skills/test-skill" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null - Copy-Item -Path $script:ValidRegistryPath -Destination "$testRepo/.github/ai-artifacts-registry.json" - - # Create the referenced artifact files - Set-Content -Path "$testRepo/.github/agents/test-agent.agent.md" -Value '# Test Agent' - Set-Content -Path "$testRepo/.github/agents/dependent-agent.agent.md" -Value '# Dependent Agent' - Set-Content -Path "$testRepo/.github/prompts/test-prompt.prompt.md" -Value '# Test Prompt' - Set-Content -Path "$testRepo/.github/instructions/test-instruction.instructions.md" -Value '# Test Instruction' - Set-Content -Path "$testRepo/.github/skills/test-skill/SKILL.md" -Value '# Test Skill' - - # Add orphan file to trigger warning - Set-Content -Path "$testRepo/.github/agents/orphan-unregistered.agent.md" -Value '# Orphan Agent' - - $output = & $script:MainScriptPath -RepoRoot $testRepo -OutputPath "$testRepo/logs/results.json" 2>&1 | Out-String - # Should have warning annotation for orphan file - $output | Should -Match '::warning' - } - - It 'Writes CI error annotation on exception in GitHub Actions' { - $env:GITHUB_ACTIONS = 'true' - $nonexistentRepo = '/completely/nonexistent/path/for/exception/test' - - $output = & $script:MainScriptPath -RepoRoot $nonexistentRepo 2>&1 | Out-String - # Should have error annotation for exception - $output | Should -Match '::error' - } - } - - Context 'Exit code handling' { - It 'Returns exit code 0 on success' { - $testRepo = Join-Path $TestDrive 'success-repo' - New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/agents" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/prompts" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/instructions" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/skills/test-skill" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null - Copy-Item -Path $script:ValidRegistryPath -Destination "$testRepo/.github/ai-artifacts-registry.json" - - # Create the referenced artifact files - Set-Content -Path "$testRepo/.github/agents/test-agent.agent.md" -Value '# Test Agent' - Set-Content -Path "$testRepo/.github/agents/dependent-agent.agent.md" -Value '# Dependent Agent' - Set-Content -Path "$testRepo/.github/prompts/test-prompt.prompt.md" -Value '# Test Prompt' - Set-Content -Path "$testRepo/.github/instructions/test-instruction.instructions.md" -Value '# Test Instruction' - Set-Content -Path "$testRepo/.github/skills/test-skill/SKILL.md" -Value '# Test Skill' - - # Run directly with output suppression - $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo' -OutputPath '$testRepo/logs/results.json'; exit `$LASTEXITCODE" 2>&1 - $LASTEXITCODE | Should -Be 0 - } - - It 'Returns exit code 0 on success with default OutputPath' { - $testRepo = Join-Path $TestDrive 'success-default-output-repo' - New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/agents" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/prompts" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/instructions" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/skills/test-skill" -Force | Out-Null - Copy-Item -Path $script:ValidRegistryPath -Destination "$testRepo/.github/ai-artifacts-registry.json" - - # Create the referenced artifact files - Set-Content -Path "$testRepo/.github/agents/test-agent.agent.md" -Value '# Test Agent' - Set-Content -Path "$testRepo/.github/agents/dependent-agent.agent.md" -Value '# Dependent Agent' - Set-Content -Path "$testRepo/.github/prompts/test-prompt.prompt.md" -Value '# Test Prompt' - Set-Content -Path "$testRepo/.github/instructions/test-instruction.instructions.md" -Value '# Test Instruction' - Set-Content -Path "$testRepo/.github/skills/test-skill/SKILL.md" -Value '# Test Skill' - - # Run without OutputPath - should use default logs/registry-validation-results.json - $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo'; exit `$LASTEXITCODE" 2>&1 - $LASTEXITCODE | Should -Be 0 - - # Verify default output path was used - Test-Path "$testRepo/logs/registry-validation-results.json" | Should -BeTrue - } - - It 'Returns exit code 1 when errors exist' { - $testRepo = Join-Path $TestDrive 'error-repo' - New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null - Copy-Item -Path $script:InvalidVersionPath -Destination "$testRepo/.github/ai-artifacts-registry.json" - - $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo' -OutputPath '$testRepo/logs/results.json'; exit `$LASTEXITCODE" 2>&1 - $LASTEXITCODE | Should -Be 1 - } - - It 'Returns exit code 1 with WarningsAsErrors and warnings' { - $testRepo = Join-Path $TestDrive 'warnings-as-errors-repo' - New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/agents" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/prompts" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/instructions" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/skills/test-skill" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null - Copy-Item -Path $script:ValidRegistryPath -Destination "$testRepo/.github/ai-artifacts-registry.json" - - # Create the referenced artifact files - Set-Content -Path "$testRepo/.github/agents/test-agent.agent.md" -Value '# Test Agent' - Set-Content -Path "$testRepo/.github/agents/dependent-agent.agent.md" -Value '# Dependent Agent' - Set-Content -Path "$testRepo/.github/prompts/test-prompt.prompt.md" -Value '# Test Prompt' - Set-Content -Path "$testRepo/.github/instructions/test-instruction.instructions.md" -Value '# Test Instruction' - Set-Content -Path "$testRepo/.github/skills/test-skill/SKILL.md" -Value '# Test Skill' - - # Add an orphan file to trigger a warning - Set-Content -Path "$testRepo/.github/agents/orphan-agent.agent.md" -Value '# Orphan' - - $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo' -OutputPath '$testRepo/logs/results.json' -WarningsAsErrors; exit `$LASTEXITCODE" 2>&1 - $LASTEXITCODE | Should -Be 1 - } - - It 'Returns exit code 0 with warnings but without WarningsAsErrors flag' { - $testRepo = Join-Path $TestDrive 'warnings-no-error-repo' - New-Item -ItemType Directory -Path "$testRepo/.git" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/agents" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/prompts" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/instructions" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/.github/skills/test-skill" -Force | Out-Null - New-Item -ItemType Directory -Path "$testRepo/logs" -Force | Out-Null - Copy-Item -Path $script:ValidRegistryPath -Destination "$testRepo/.github/ai-artifacts-registry.json" - - # Create the referenced artifact files - Set-Content -Path "$testRepo/.github/agents/test-agent.agent.md" -Value '# Test Agent' - Set-Content -Path "$testRepo/.github/agents/dependent-agent.agent.md" -Value '# Dependent Agent' - Set-Content -Path "$testRepo/.github/prompts/test-prompt.prompt.md" -Value '# Test Prompt' - Set-Content -Path "$testRepo/.github/instructions/test-instruction.instructions.md" -Value '# Test Instruction' - Set-Content -Path "$testRepo/.github/skills/test-skill/SKILL.md" -Value '# Test Skill' - - # Add an orphan file to trigger a warning - Set-Content -Path "$testRepo/.github/agents/orphan-agent.agent.md" -Value '# Orphan' - - # Without WarningsAsErrors, should still pass - $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo' -OutputPath '$testRepo/logs/results.json'; exit `$LASTEXITCODE" 2>&1 - $LASTEXITCODE | Should -Be 0 - } - } - - Context 'Exception handling' { - It 'Exits with code 1 on exception' { - # Trigger exception by providing invalid path type - $testRepo = '/nonexistent/path/that/will/cause/error' - $null = & pwsh -NoProfile -Command "& '$script:MainScriptPath' -RepoRoot '$testRepo'; exit `$LASTEXITCODE" 2>&1 - $LASTEXITCODE | Should -Be 1 - } - - It 'Writes CI error annotation when exception occurs in GitHub Actions' { - $env:GITHUB_ACTIONS = 'true' - # Use completely invalid path to trigger exception in catch block - $invalidRepo = '/this/path/does/not/exist/anywhere' - - $output = & $script:MainScriptPath -RepoRoot $invalidRepo 2>&1 | Out-String - - # Should have error annotation from the catch block - $output | Should -Match '::error.*Registry validation failed' - } - - It 'Writes CI error annotation when exception occurs in Azure DevOps' { - $env:TF_BUILD = 'True' - # Use completely invalid path to trigger exception in catch block - $invalidRepo = '/this/path/does/not/exist/anywhere' - - $output = & $script:MainScriptPath -RepoRoot $invalidRepo 2>&1 | Out-String - - # Should have error annotation from the catch block (Azure DevOps format) - $output | Should -Match '##vso\[task\.logissue.*error.*Registry validation failed' - } - } -} - -#endregion - -#region Edge Cases Tests - -Describe 'Edge Cases' -Tag 'Unit' { - Context 'Empty registry sections' { - It 'Handles registry with no agents' { - $registry = @{ - agents = @{} - prompts = @{ 'p1' = @{} } - instructions = @{} - skills = @{} - } - Mock Test-Path { return $true } - $result = Test-ArtifactFileExistence -Registry $registry -RepoRoot $TestDrive - $result.Success | Should -BeTrue - } - - It 'Test-ArtifactEntries succeeds with empty sections' { - $registry = @{ - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Test-ArtifactEntries -Registry $registry - $result.Success | Should -BeTrue - $result.Errors.Count | Should -Be 0 - } - - It 'Handles empty personas definitions' { - $registry = @{ - personas = @{ definitions = @{} } - agents = @{} - prompts = @{} - instructions = @{} - skills = @{} - } - $result = Test-PersonaReferences -Registry $registry - $result.Success | Should -BeTrue - } - } - - Context 'Complex dependency chains' { - It 'Handles deeply nested non-circular dependencies' { - $registry = @{ - agents = @{ - 'agent-1' = @{ requires = @{ agents = @('agent-2') } } - 'agent-2' = @{ requires = @{ agents = @('agent-3') } } - 'agent-3' = @{ requires = @{ agents = @('agent-4') } } - 'agent-4' = @{ requires = @{ agents = @('agent-5') } } - 'agent-5' = @{ requires = @{ agents = @() } } - } - } - $result = Find-CircularAgentDependencies -Registry $registry - $result.Count | Should -Be 0 - } - - It 'Handles diamond dependency pattern without cycles' { - $registry = @{ - agents = @{ - 'top' = @{ requires = @{ agents = @('left', 'right') } } - 'left' = @{ requires = @{ agents = @('bottom') } } - 'right' = @{ requires = @{ agents = @('bottom') } } - 'bottom' = @{ requires = @{ agents = @() } } - } - } - $result = Find-CircularAgentDependencies -Registry $registry - $result.Count | Should -Be 0 - } - } -} - -#endregion From 29776208f299f4d6387b2001987a7ad324a58ac7 Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 18:54:33 -0800 Subject: [PATCH 44/62] refactor(tests): remove mock registry and simplify agent and skill tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - delete unused ai-artifacts-registry.schema.json - streamline agent and skill discovery tests by removing mock registry - adjust tests to directly use discovered agents and skills - enhance clarity in filtering tests for agents and skills ๐Ÿ”ง - Generated by Copilot --- .github/agents/hve-core-installer.agent.md | 6 +- .github/ai-artifacts-registry.json | 974 ------------------ .github/copilot-instructions.md | 2 + .../prompt-builder.instructions.md | 2 +- .github/prompts/pull-request.prompt.md | 15 +- collections/hve-core-all.collection.yml | 2 +- collections/rpi.collection.yml | 10 - docs/architecture/ai-artifacts.md | 72 +- docs/architecture/workflows.md | 94 +- docs/contributing/ai-artifacts-common.md | 89 +- docs/contributing/custom-agents.md | 46 +- docs/contributing/instructions.md | 34 +- docs/contributing/prompts.md | 23 +- docs/contributing/release-process.md | 12 +- docs/contributing/skills.md | 21 +- scripts/extension/Prepare-Extension.ps1 | 256 ++--- .../schemas/ai-artifacts-registry.schema.json | 195 ---- .../extension/Prepare-Extension.Tests.ps1 | 279 ++--- 18 files changed, 326 insertions(+), 1806 deletions(-) delete mode 100644 .github/ai-artifacts-registry.json delete mode 100644 scripts/linting/schemas/ai-artifacts-registry.schema.json diff --git a/.github/agents/hve-core-installer.agent.md b/.github/agents/hve-core-installer.agent.md index 4c481a70..497e61de 100644 --- a/.github/agents/hve-core-installer.agent.md +++ b/.github/agents/hve-core-installer.agent.md @@ -1081,11 +1081,11 @@ User input handling: ### Persona Selection Sub-Flow -When the user selects option 2, read the artifact registry to present available personas. +When the user selects option 2, read collection manifests to present available personas. -#### Step 1: Read registry and build persona agent counts +#### Step 1: Read collections and build persona agent counts -Read `.github/ai-artifacts-registry.json` from the HVE-Core source (at `$hveCoreBasePath`). Parse `personas.definitions` for display names and descriptions. For each agent entry, count stable agents per persona (exclude `experimental` and `deprecated` maturity). +Read `collections/*.collection.yml` from the HVE-Core source (at `$hveCoreBasePath`). Derive persona options from collection `id` and `name`. For each selected collection, count agent items where `kind` equals `agent` and effective item maturity is `stable` (item `maturity` omitted defaults to `stable`; exclude `experimental` and `deprecated`). #### Step 2: Present persona options diff --git a/.github/ai-artifacts-registry.json b/.github/ai-artifacts-registry.json deleted file mode 100644 index 60f81d04..00000000 --- a/.github/ai-artifacts-registry.json +++ /dev/null @@ -1,974 +0,0 @@ -{ - "$schema": "../scripts/linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { - "definitions": { - "hve-core-all": { - "name": "HVE Core All", - "description": "Full HVE-Core release including all artifacts regardless of role-specific filtering" - }, - "developer": { - "name": "Developer", - "description": "Software engineers writing code" - }, - "tpm": { - "name": "Technical PM", - "description": "Program/product managers" - }, - "devops": { - "name": "DevOps Engineer", - "description": "Platform, SRE, infrastructure engineers" - }, - "architect": { - "name": "Software Architect", - "description": "Solution/system architects" - }, - "technical-writer": { - "name": "Technical Writer", - "description": "Documentation specialists" - } - } - }, - "agents": { - "ado-prd-to-wit": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "ado", - "planning" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [ - "ado-wit-planning" - ], - "skills": [] - } - }, - "adr-creation": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "architecture", - "documentation" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "arch-diagram-builder": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "architecture", - "documentation" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "brd-builder": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "planning", - "documentation" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "doc-ops": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "documentation" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [ - "writing-style", - "markdown", - "commit-message" - ], - "skills": [] - } - }, - "gen-data-spec": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "data-science" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "gen-jupyter-notebook": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "data-science" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "gen-streamlit-dashboard": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "data-science" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "github-backlog-manager": { - "maturity": "experimental", - "personas": [ - "hve-core-all" - ], - "tags": [ - "github", - "backlog", - "orchestration" - ], - "requires": { - "agents": [ - "memory" - ], - "prompts": [ - "github-discover-issues", - "github-triage-issues", - "github-sprint-plan", - "github-execute-backlog", - "checkpoint" - ], - "instructions": [ - "github-backlog-discovery", - "github-backlog-planning", - "github-backlog-triage", - "github-backlog-update", - "community-interaction" - ], - "skills": [] - } - }, - "github-issue-manager": { - "maturity": "deprecated", - "personas": [ - "hve-core-all" - ], - "tags": [ - "github" - ], - "requires": { - "agents": [], - "prompts": [ - "github-add-issue" - ], - "instructions": [ - "markdown" - ], - "skills": [] - } - }, - "hve-core-installer": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "tooling" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "memory": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "rpi", - "session" - ], - "requires": { - "agents": [], - "prompts": [ - "checkpoint", - "rpi" - ], - "instructions": [], - "skills": [] - } - }, - "pr-review": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "github", - "review" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "prd-builder": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "planning", - "documentation" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "prompt-builder": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "prompt-engineering" - ], - "requires": { - "agents": [], - "prompts": [ - "prompt-build", - "prompt-refactor", - "prompt-analyze" - ], - "instructions": [ - "prompt-builder" - ], - "skills": [] - } - }, - "rpi-agent": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer", - "tpm", - "devops", - "architect", - "technical-writer" - ], - "tags": [ - "rpi", - "orchestration" - ], - "requires": { - "agents": [ - "task-researcher", - "task-planner", - "task-implementor", - "task-reviewer" - ], - "prompts": [ - "task-research", - "task-plan", - "task-implement", - "task-review", - "rpi", - "checkpoint" - ], - "instructions": [], - "skills": [ - "video-to-gif" - ] - } - }, - "security-plan-creator": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "architecture", - "security" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "task-implementor": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "rpi", - "implementation" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [ - "commit-message" - ], - "skills": [] - } - }, - "task-planner": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "rpi", - "planning" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "task-researcher": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "rpi", - "research" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "task-reviewer": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "rpi", - "review" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - }, - "test-streamlit-dashboard": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "data-science", - "testing" - ], - "requires": { - "agents": [], - "prompts": [], - "instructions": [], - "skills": [] - } - } - }, - "prompts": { - "ado-create-pull-request": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "ado", - "git" - ] - }, - "ado-get-build-info": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "ado" - ] - }, - "ado-get-my-work-items": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "ado" - ] - }, - "ado-process-my-work-items-for-task-planning": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "ado", - "planning" - ] - }, - "ado-update-wit-items": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "ado" - ] - }, - "checkpoint": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "rpi", - "session" - ] - }, - "doc-ops-update": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "documentation" - ] - }, - "git-commit": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "git" - ] - }, - "git-commit-message": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "git" - ] - }, - "git-merge": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "git" - ] - }, - "git-setup": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "git" - ] - }, - "github-add-issue": { - "maturity": "deprecated", - "personas": [ - "hve-core-all" - ], - "tags": [ - "github" - ] - }, - "github-discover-issues": { - "maturity": "experimental", - "personas": [ - "hve-core-all" - ], - "tags": [ - "github", - "backlog" - ] - }, - "github-execute-backlog": { - "maturity": "experimental", - "personas": [ - "hve-core-all" - ], - "tags": [ - "github", - "backlog" - ] - }, - "github-sprint-plan": { - "maturity": "experimental", - "personas": [ - "hve-core-all" - ], - "tags": [ - "github", - "backlog" - ] - }, - "github-triage-issues": { - "maturity": "experimental", - "personas": [ - "hve-core-all" - ], - "tags": [ - "github", - "backlog" - ] - }, - "incident-response": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "operations", - "incident" - ] - }, - "prompt-analyze": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "prompt-engineering" - ] - }, - "prompt-build": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "prompt-engineering" - ] - }, - "prompt-refactor": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "prompt-engineering" - ] - }, - "pull-request": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "github", - "git" - ] - }, - "risk-register": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "planning" - ] - }, - "rpi": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer", - "tpm", - "devops", - "architect", - "technical-writer" - ], - "tags": [ - "rpi" - ] - }, - "task-implement": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "rpi", - "implementation" - ] - }, - "task-plan": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "rpi", - "planning" - ] - }, - "task-research": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "rpi", - "research" - ] - }, - "task-review": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "rpi", - "review" - ] - } - }, - "instructions": { - "ado-create-pull-request": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "ado", - "git" - ] - }, - "ado-get-build-info": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "ado" - ] - }, - "ado-update-wit-items": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "ado" - ] - }, - "ado-wit-discovery": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "ado" - ] - }, - "ado-wit-planning": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "ado", - "planning" - ] - }, - "commit-message": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "git" - ] - }, - "community-interaction": { - "maturity": "experimental", - "personas": [ - "hve-core-all" - ], - "tags": [ - "github", - "documentation" - ] - }, - "git-merge": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "git" - ] - }, - "github-backlog-discovery": { - "maturity": "experimental", - "personas": [ - "hve-core-all" - ], - "tags": [ - "github", - "backlog" - ] - }, - "github-backlog-planning": { - "maturity": "experimental", - "personas": [ - "hve-core-all" - ], - "tags": [ - "github", - "backlog" - ] - }, - "github-backlog-triage": { - "maturity": "experimental", - "personas": [ - "hve-core-all" - ], - "tags": [ - "github", - "backlog" - ] - }, - "github-backlog-update": { - "maturity": "experimental", - "personas": [ - "hve-core-all" - ], - "tags": [ - "github", - "backlog" - ] - }, - "hve-core-location": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "tooling" - ] - }, - "markdown": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer", - "tpm", - "devops", - "architect", - "technical-writer" - ], - "tags": [ - "documentation" - ] - }, - "prompt-builder": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "prompt-engineering" - ] - }, - "python-script": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "language" - ] - }, - "uv-projects": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "language" - ] - }, - "writing-style": { - "maturity": "stable", - "personas": [ - "hve-core-all" - ], - "tags": [ - "documentation" - ] - }, - "bash/bash": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "language" - ] - }, - "bicep/bicep": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "language", - "infrastructure" - ] - }, - "csharp/csharp": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "language" - ] - }, - "csharp/csharp-tests": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "language", - "testing" - ] - }, - "terraform/terraform": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "language", - "infrastructure" - ] - } - }, - "skills": { - "video-to-gif": { - "maturity": "stable", - "personas": [ - "hve-core-all", - "developer" - ], - "tags": [ - "media", - "tooling" - ] - } - } -} \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e9fd39bf..21ad4108 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -90,6 +90,8 @@ All tracking files use markdown format with frontmatter and follow patterns from * Scripts follow instructions provided by the codebase for convention and standards. * Scripts used by the codebase have an `npm run` script for ease of use. +* Files under the root `plugins/` directory are generated outputs and are not edited directly. +* Regenerate plugin outputs using `npm run plugin:generate`; markdown files under `plugins/` can be symlinked or generated, so direct edits can cause conflicts and non-durable changes. PowerShell scripts follow PSScriptAnalyzer rules from `PSScriptAnalyzer.psd1` and include proper comment-based help. Validation runs via `npm run lint:ps` with results output to `logs/`. diff --git a/.github/instructions/prompt-builder.instructions.md b/.github/instructions/prompt-builder.instructions.md index 5940c3dd..78b3f3af 100644 --- a/.github/instructions/prompt-builder.instructions.md +++ b/.github/instructions/prompt-builder.instructions.md @@ -213,7 +213,7 @@ Validation guidelines: This section defines frontmatter field requirements for prompt engineering artifacts. -Maturity is tracked in `.github/ai-artifacts-registry.json`, not in frontmatter. Do not include a `maturity` field in artifact frontmatter. Set the maturity value in the artifact's registry entry instead. +Maturity is tracked in `collections/*.collection.yml` item metadata, not in frontmatter. Do not include a `maturity` field in artifact frontmatter. Set maturity on the artifact's matching collection item entry; when omitted, maturity defaults to `stable`. ### Required Fields diff --git a/.github/prompts/pull-request.prompt.md b/.github/prompts/pull-request.prompt.md index 78091adc..76b8b00d 100644 --- a/.github/prompts/pull-request.prompt.md +++ b/.github/prompts/pull-request.prompt.md @@ -134,11 +134,12 @@ Analyze changed files from the `` section of `pr-reference.xml` and e | Copilot instructions | `.*\.instructions\.md$` | N/A | N/A | | Copilot prompt | `.*\.prompt\.md$` | N/A | N/A | | Copilot agent | `.*\.agent\.md$` | N/A | N/A | +| Copilot skill | `.*/SKILL\.md$` | N/A | N/A | | Script or automation | `.*\.(ps1\|sh\|py)$` | N/A | N/A | Priority rules: -* AI artifact patterns (`.instructions.md`, `.prompt.md`, `.agent.md`) take precedence over documentation updates. +* AI artifact patterns (`.instructions.md`, `.prompt.md`, `.agent.md`, `SKILL.md`) take precedence over documentation updates. * Any breaking change in commits marks the PR as breaking. * Multiple change types can be selected. @@ -159,14 +160,11 @@ Deduplicate issue numbers and preserve the action prefix from the first occurren #### GHCP Maturity Detection -After detecting GHCP files from Change Type Detection, look up maturity levels from the AI Artifacts Registry: +After detecting GHCP files from Change Type Detection, look up maturity levels from collection manifest item metadata: -1. For each file matching `.instructions.md`, `.prompt.md`, or `.agent.md` patterns: - * Derive the artifact key from the file path by removing the extension pattern (e.g., `pull-request.prompt.md` โ†’ `pull-request`, `bash/bash.instructions.md` โ†’ `bash/bash`) - * Look up the artifact's `maturity` value in `.github/ai-artifacts-registry.json` - * Default to `stable` if the artifact is not found in the registry +1. For each file matching `.instructions.md`, `.prompt.md`, `.agent.md`, or `SKILL.md` patterns, find matching entries in `collections/*.collection.yml`, read each item's optional `maturity`, use `stable` when omitted, and when the same file appears in multiple collections use the highest-risk effective value in this order: `deprecated`, `experimental`, `preview`, `stable`. -2. Categorize files by maturity: +1. Categorize files by maturity: | Maturity Level | Risk Level | Indicator | Action | |----------------|-------------|---------------------------|---------------------------------| @@ -175,7 +173,7 @@ After detecting GHCP files from Change Type Detection, look up maturity levels f | experimental | โš ๏ธ High | May have breaking changes | Add warning banner | | deprecated | ๐Ÿšซ Critical | Scheduled for removal | Add deprecation notice | -3. If non-stable GHCP files detected, generate "GHCP Artifact Maturity" section in `pr.md` +1. If non-stable GHCP files are detected, generate a "GHCP Artifact Maturity" section in `pr.md`. #### GHCP Maturity Output @@ -210,6 +208,7 @@ Always include when any GHCP files are detected: |--------------------------|--------------|-----------------|------------------| | `new-feature.prompt.md` | Prompt | โš ๏ธ experimental | Pre-release only | | `helper.agent.md` | Agent | ๐Ÿ”ถ preview | Pre-release only | +| `video-to-gif/SKILL.md` | Skill | โœ… stable | All builds | | `coding.instructions.md` | Instructions | โœ… stable | All builds | ``` diff --git a/collections/hve-core-all.collection.yml b/collections/hve-core-all.collection.yml index cfbba59f..9cdcb52e 100644 --- a/collections/hve-core-all.collection.yml +++ b/collections/hve-core-all.collection.yml @@ -155,5 +155,5 @@ items: - path: .github/skills/video-to-gif kind: skill display: - ordering: alpha featured: true + ordering: alpha diff --git a/collections/rpi.collection.yml b/collections/rpi.collection.yml index ab0b6e86..4583a081 100644 --- a/collections/rpi.collection.yml +++ b/collections/rpi.collection.yml @@ -12,11 +12,6 @@ items: # Agents - path: .github/agents/rpi-agent.agent.md kind: agent - usage: | - recommended - - Orchestrator agent for the full RPI lifecycle. Routes to specialized - agents based on the current workflow phase. - path: .github/agents/task-researcher.agent.md kind: agent - path: .github/agents/task-planner.agent.md @@ -27,11 +22,6 @@ items: kind: agent - path: .github/agents/memory.agent.md kind: agent - usage: | - optional - - Persistent memory agent for saving and loading context checkpoints - across workflow phases. # Prompt Engineering agents - path: .github/agents/prompt-builder.agent.md kind: agent diff --git a/docs/architecture/ai-artifacts.md b/docs/architecture/ai-artifacts.md index addc9033..b4fcb4b3 100644 --- a/docs/architecture/ai-artifacts.md +++ b/docs/architecture/ai-artifacts.md @@ -80,7 +80,7 @@ Instructions answer the question "what standards apply to this context?" and ens #### Repo-Specific Instructions -Instructions placed in `.github/instructions/hve-core/` are scoped to the hve-core repository itself and MUST NOT be registered as AI artifacts. These files govern internal repository concerns (CI/CD workflows, repo-specific conventions) that are not applicable outside the repository. The build system and registry validation automatically exclude this subdirectory from artifact discovery and orphan detection. +Instructions placed in `.github/instructions/hve-core/` are scoped to the hve-core repository itself and MUST NOT be included in collection manifests. These files govern internal repository concerns (CI/CD workflows, repo-specific conventions) that are not applicable outside the repository. Collection manifests intentionally exclude this subdirectory from artifact selection and package composition. > [!IMPORTANT] > The `.github/instructions/hve-core/` directory is reserved for repo-specific instructions. Files in this directory are never distributed through extension packages or persona collections. @@ -123,7 +123,7 @@ description: 'Video-to-GIF conversion with FFmpeg optimization' | `name` | Lowercase kebab-case identifier matching directory name | | `description` | Brief capability description | -Maturity is tracked in the artifact registry, not in skill frontmatter. See [Artifact Registry](#artifact-registry) for details. +Maturity is tracked in `collections/*.collection.yml`, not in skill frontmatter. See [Collection Manifests](#collection-manifests) for details. Skills answer the question "what specialized utility does this task require?" and provide executable capabilities beyond conversational guidance. @@ -201,23 +201,22 @@ Skills provide self-contained utilities through the `SKILL.md` file: Copilot discovers skills automatically when their description matches the current task context. Skills can also be referenced explicitly by name. The skill's `SKILL.md` documents prerequisites, parameters, and usage patterns. Cross-platform scripts ensure consistent behavior across operating systems. -## Artifact Registry +## Collection Manifests -The artifact registry (`.github/ai-artifacts-registry.json`) serves as the central metadata store for all AI artifacts. It enables persona-based distribution, maturity filtering, and dependency resolution without polluting individual artifact frontmatter. +Collection manifests in `collections/*.collection.yml` serve as the source of truth for artifact selection and maturity. They drive packaging for extension collections and contributor workflows without adding maturity metadata to artifact frontmatter. -### Registry Architecture +### Collection Architecture ```text โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ AI Artifacts Registry โ”‚ -โ”‚ .github/ai-artifacts-registry.json โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Agents โ”‚ Prompts โ”‚ Instructions โ”‚ โ”‚ -โ”‚ โ”‚ - maturity โ”‚ - maturity โ”‚ - maturity โ”‚ โ”‚ -โ”‚ โ”‚ - personas[] โ”‚ - personas[] โ”‚ - personas[] โ”‚ โ”‚ -โ”‚ โ”‚ - tags[] โ”‚ - tags[] โ”‚ - tags[] โ”‚ โ”‚ -โ”‚ โ”‚ - requires{} โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ Collection Manifests โ”‚ +โ”‚ collections/*.collection.yml โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ items[] โ”‚ โ”‚ +โ”‚ โ”‚ - path โ”‚ โ”‚ +โ”‚ โ”‚ - kind โ”‚ โ”‚ +โ”‚ โ”‚ - maturity (optional, defaults to stable) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ @@ -226,41 +225,34 @@ The artifact registry (`.github/ai-artifacts-registry.json`) serves as the centr โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Collection โ”‚ โ”‚ Prepare- โ”‚ โ”‚ โ”‚ โ”‚ Manifests โ”‚โ”€โ”€โ”€โ–ถโ”‚ Extension.ps1 โ”‚ โ”‚ -โ”‚ โ”‚ *.collection.json โ”‚ -Collection โ”‚ โ”‚ +โ”‚ โ”‚ *.collection.yml โ”‚ -Collection โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` -### Registry Entry Structure +### Collection Item Structure -Each artifact entry contains metadata for filtering and dependency resolution: +Each collection item defines inclusion metadata for artifact selection and release channel filtering: -```json -{ - "artifact-name": { - "maturity": "stable", - "personas": ["hve-core-all", "developer"], - "tags": ["rpi", "workflow"], - "requires": { - "agents": ["dependency-agent"], - "prompts": ["dependency-prompt"], - "instructions": ["dependency-instructions"], - "skills": [] - } - } -} +```yaml +items: + - path: .github/agents/rpi-agent.agent.md + kind: agent + maturity: stable + - path: .github/prompts/task-plan.prompt.md + kind: prompt + maturity: preview ``` -| Field | Purpose | -|------------|-------------------------------------------------| -| `maturity` | Controls extension channel inclusion | -| `personas` | Determines collection membership | -| `tags` | Categorization for organization and discovery | -| `requires` | Declares dependencies for complete installation | +| Field | Purpose | +|------------|-------------------------------------------------------------------| +| `path` | Repository-relative path to the artifact source | +| `kind` | Artifact type (`agent`, `prompt`, `instruction`, `skill`, `hook`) | +| `maturity` | Optional release channel gating value (`stable` default) | ### Persona Model -Personas represent user roles that consume artifacts. The registry defines these personas: +Personas represent user roles that consume artifacts. Collection manifests select artifacts for those personas. | Persona | Identifier | Target Users | |---------------|----------------|---------------------| @@ -288,7 +280,7 @@ The build system resolves collections by: 1. Reading the collection manifest to identify target personas 2. Checking collection-level maturity against the target release channel -3. Filtering registry entries by persona membership +3. Filtering collection items by path/kind membership 4. Including the `hve-core-all` persona artifacts as the base 5. Adding persona-specific artifacts 6. Resolving dependencies for included artifacts @@ -335,7 +327,7 @@ The extension scans these directories at startup: * `.github/instructions/` for technology standards (excluding `hve-core/` subdirectory) * `.github/skills/` for utility packages -Artifact inclusion is controlled by the registry. Repo-specific instructions under `.github/instructions/hve-core/` are excluded from discovery and never packaged into extension builds. +Artifact inclusion is controlled by `collections/*.collection.yml`. Repo-specific instructions under `.github/instructions/hve-core/` are excluded from discovery and never packaged into extension builds. | Maturity | Stable Channel | Pre-release Channel | |----------------|----------------|---------------------| diff --git a/docs/architecture/workflows.md b/docs/architecture/workflows.md index 5b2e3770..0354bf23 100644 --- a/docs/architecture/workflows.md +++ b/docs/architecture/workflows.md @@ -55,28 +55,27 @@ flowchart TD Individual validation workflows called by orchestration workflows: -| Workflow | Purpose | npm Script | -|------------------------------------|---------------------------------|----------------------------| -| `markdown-lint.yml` | Markdownlint validation | `npm run lint:md` | -| `spell-check.yml` | cspell dictionary check | `npm run spell-check` | -| `frontmatter-validation.yml` | AI artifact frontmatter schemas | `npm run lint:frontmatter` | -| `markdown-link-check.yml` | Broken link detection | `npm run lint:md-links` | -| `link-lang-check.yml` | Link language validation | `npm run lint:links` | -| `yaml-lint.yml` | YAML syntax validation | `npm run lint:yaml` | -| `ps-script-analyzer.yml` | PowerShell static analysis | `npm run lint:ps` | -| `table-format.yml` | Markdown table formatting | `npm run format:tables` | -| `pester-tests.yml` | PowerShell unit tests | `npm run test:ps` | -| `dependency-pinning-scan.yml` | GitHub Actions pinning | N/A (PowerShell direct) | -| `sha-staleness-check.yml` | SHA reference freshness | N/A (PowerShell direct) | -| `codeql-analysis.yml` | CodeQL security scanning | N/A (GitHub native) | -| `dependency-review.yml` | Dependency vulnerability review | N/A (GitHub native) | -| `artifact-registry-validation.yml` | AI artifact registry validation | `npm run lint:registry` | -| `security-scan.yml` | Composite security validation | N/A | -| `extension-package.yml` | VS Code extension packaging | N/A | +| Workflow | Purpose | npm Script | +|-------------------------------|---------------------------------|----------------------------| +| `markdown-lint.yml` | Markdownlint validation | `npm run lint:md` | +| `spell-check.yml` | cspell dictionary check | `npm run spell-check` | +| `frontmatter-validation.yml` | AI artifact frontmatter schemas | `npm run lint:frontmatter` | +| `markdown-link-check.yml` | Broken link detection | `npm run lint:md-links` | +| `link-lang-check.yml` | Link language validation | `npm run lint:links` | +| `yaml-lint.yml` | YAML syntax validation | `npm run lint:yaml` | +| `ps-script-analyzer.yml` | PowerShell static analysis | `npm run lint:ps` | +| `table-format.yml` | Markdown table formatting | `npm run format:tables` | +| `pester-tests.yml` | PowerShell unit tests | `npm run test:ps` | +| `dependency-pinning-scan.yml` | GitHub Actions pinning | N/A (PowerShell direct) | +| `sha-staleness-check.yml` | SHA reference freshness | N/A (PowerShell direct) | +| `codeql-analysis.yml` | CodeQL security scanning | N/A (GitHub native) | +| `dependency-review.yml` | Dependency vulnerability review | N/A (GitHub native) | +| `security-scan.yml` | Composite security validation | N/A | +| `extension-package.yml` | VS Code extension packaging | N/A | ## PR Validation Pipeline -The `pr-validation.yml` workflow serves as the primary quality gate for all pull requests. It runs 13 parallel jobs covering linting, security, and testing. +The `pr-validation.yml` workflow serves as the primary quality gate for all pull requests. It runs 12 parallel jobs covering linting, security, and testing. ```mermaid flowchart LR @@ -86,16 +85,15 @@ flowchart LR TF[table-format] YL[yaml-lint] FV[frontmatter-validation] - ARV[artifact-registry-validation] LLC[link-lang-check] MLC[markdown-link-check] end - + subgraph "Analysis" PSA[psscriptanalyzer] PT[pester-tests] end - + subgraph "Security" DPC[dependency-pinning-check] NA[npm-audit] @@ -105,21 +103,20 @@ flowchart LR ### Jobs -| Job | Reusable Workflow | Validates | -|------------------------------|------------------------------------|--------------------------------| -| spell-check | `spell-check.yml` | Spelling across all files | -| markdown-lint | `markdown-lint.yml` | Markdown formatting rules | -| table-format | `table-format.yml` | Markdown table structure | -| psscriptanalyzer | `ps-script-analyzer.yml` | PowerShell code quality | -| yaml-lint | `yaml-lint.yml` | YAML syntax | -| pester-tests | `pester-tests.yml` | PowerShell unit tests | -| frontmatter-validation | `frontmatter-validation.yml` | AI artifact metadata | -| link-lang-check | `link-lang-check.yml` | Link accessibility | -| markdown-link-check | `markdown-link-check.yml` | Broken links | -| artifact-registry-validation | `artifact-registry-validation.yml` | AI artifact registry integrity | -| dependency-pinning-check | `dependency-pinning-scan.yml` | Action SHA pinning | -| npm-audit | Inline | npm dependency vulnerabilities | -| codeql | `codeql-analysis.yml` | Code security patterns | +| Job | Reusable Workflow | Validates | +|--------------------------|-------------------------------|--------------------------------| +| spell-check | `spell-check.yml` | Spelling across all files | +| markdown-lint | `markdown-lint.yml` | Markdown formatting rules | +| table-format | `table-format.yml` | Markdown table structure | +| psscriptanalyzer | `ps-script-analyzer.yml` | PowerShell code quality | +| yaml-lint | `yaml-lint.yml` | YAML syntax | +| pester-tests | `pester-tests.yml` | PowerShell unit tests | +| frontmatter-validation | `frontmatter-validation.yml` | AI artifact metadata | +| link-lang-check | `link-lang-check.yml` | Link accessibility | +| markdown-link-check | `markdown-link-check.yml` | Broken links | +| dependency-pinning-check | `dependency-pinning-scan.yml` | Action SHA pinning | +| npm-audit | Inline | npm dependency vulnerabilities | +| codeql | `codeql-analysis.yml` | Code security patterns | All jobs run in parallel with no dependencies, enabling fast feedback (typically under 3 minutes). @@ -227,18 +224,17 @@ Maturity filtering rules: Workflows invoke validation through npm scripts defined in `package.json`: -| npm Script | Command | Used By | -|--------------------|------------------------------------|----------------------------------| -| `lint:md` | `markdownlint-cli2` | markdown-lint.yml | -| `spell-check` | `cspell` | spell-check.yml | -| `lint:frontmatter` | `Validate-MarkdownFrontmatter.ps1` | frontmatter-validation.yml | -| `lint:md-links` | `Markdown-Link-Check.ps1` | markdown-link-check.yml | -| `lint:links` | `Invoke-LinkLanguageCheck.ps1` | link-lang-check.yml | -| `lint:yaml` | `Invoke-YamlLint.ps1` | yaml-lint.yml | -| `lint:ps` | `Invoke-PSScriptAnalyzer.ps1` | ps-script-analyzer.yml | -| `format:tables` | `markdown-table-formatter` | table-format.yml | -| `lint:registry` | `Validate-ArtifactRegistry.ps1` | artifact-registry-validation.yml | -| `test:ps` | `Invoke-Pester` | pester-tests.yml | +| npm Script | Command | Used By | +|--------------------|------------------------------------|----------------------------| +| `lint:md` | `markdownlint-cli2` | markdown-lint.yml | +| `spell-check` | `cspell` | spell-check.yml | +| `lint:frontmatter` | `Validate-MarkdownFrontmatter.ps1` | frontmatter-validation.yml | +| `lint:md-links` | `Markdown-Link-Check.ps1` | markdown-link-check.yml | +| `lint:links` | `Invoke-LinkLanguageCheck.ps1` | link-lang-check.yml | +| `lint:yaml` | `Invoke-YamlLint.ps1` | yaml-lint.yml | +| `lint:ps` | `Invoke-PSScriptAnalyzer.ps1` | ps-script-analyzer.yml | +| `format:tables` | `markdown-table-formatter` | table-format.yml | +| `test:ps` | `Invoke-Pester` | pester-tests.yml | ## Related Documentation diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index ebc9e47d..35f8d081 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -85,77 +85,68 @@ All AI artifacts (agents, instructions, prompts) **MUST** target the **latest av 3. **Performance**: Latest models provide superior reasoning, accuracy, and efficiency 4. **Future-proofing**: Older models will be deprecated and removed from service -## Artifact Registry +## Collection Manifests -All AI artifacts are registered in `.github/ai-artifacts-registry.json`, a centralized metadata file that controls distribution, persona filtering, and dependency resolution without polluting individual artifact frontmatter. +Collection manifests in `collections/*.collection.yml` are the source of truth for artifact selection and maturity. -### Registry Purpose +### Collection Purpose -The registry serves three primary functions: +Collection manifests serve three primary functions: -1. **Maturity filtering**: Controls which extension channel (stable vs pre-release) includes each artifact -2. **Persona filtering**: Determines which artifacts appear in role-specific collection packages -3. **Dependency resolution**: Declares explicit dependencies between artifacts for complete installation +1. **Selection**: Determine which artifacts are included in each collection via `items[]` +2. **Maturity filtering**: Control channel inclusion with `items[].maturity` (defaults to `stable`) +3. **Packaging inputs**: Provide canonical manifest data used by build and distribution flows -### Registry Structure +### Collection Structure -The registry contains four top-level sections: +Each manifest contains top-level collection metadata and an `items` array: -```json -{ - "$schema": "../scripts/linting/schemas/ai-artifacts-registry.schema.json", - "version": "1.0", - "personas": { /* persona definitions */ }, - "agents": { /* agent entries */ }, - "prompts": { /* prompt entries */ }, - "instructions": { /* instruction entries */ }, - "skills": { /* skill entries */ } -} +```yaml +id: coding-standards +name: Coding Standards +description: Language-specific coding instructions +items: + - path: .github/instructions/python-script.instructions.md + kind: instruction + maturity: stable + - path: .github/prompts/task-plan.prompt.md + kind: prompt + maturity: preview ``` -### Artifact Entry Format +### Collection Item Format -Each artifact entry follows this structure: +Each `items[]` entry follows this structure: -```json -"artifact-name": { - "maturity": "stable", - "personas": ["hve-core-all", "developer"], - "tags": ["rpi", "planning"], - "requires": { - "agents": [], - "prompts": ["task-plan"], - "instructions": ["commit-message"], - "skills": [] - } -} +```yaml +- path: .github/agents/rpi-agent.agent.md + kind: agent + maturity: stable ``` -| Field | Required | Description | -|------------|----------|------------------------------------------------------| -| `maturity` | Yes | Release readiness level | -| `personas` | Yes | Array of persona identifiers for collection building | -| `tags` | Yes | Categorization tags for organization and search | -| `requires` | No | Dependency declarations for complete installation | +| Field | Required | Description | +|------------|----------|--------------------------------------------------------------------------------| +| `path` | Yes | Repository-relative path to the artifact source | +| `kind` | Yes | Artifact type (`agent`, `prompt`, `instruction`, `skill`, or `hook`) | +| `maturity` | No | Release readiness level; when omitted, effective maturity defaults to `stable` | -### Adding Artifacts to the Registry +### Adding Artifacts to a Collection When contributing a new artifact: 1. Create the artifact file in the appropriate directory -2. Add an entry to the registry under the correct section (`agents`, `prompts`, `instructions`, or `skills`) -3. Set appropriate `maturity`, `personas`, and `tags` values -4. Declare any dependencies in `requires` if the artifact depends on other artifacts -5. Run `npm run lint:registry` to validate the registry entry +2. Add a matching `items[]` entry in one or more `collections/*.collection.yml` files +3. Set `maturity` when the artifact should be `preview`, `experimental`, or `deprecated` +4. Run `npm run lint:yaml` to validate manifest syntax and schema compliance ### Repo-Specific Instructions Exclusion -Instructions placed in `.github/instructions/hve-core/` are repo-specific and MUST NOT be added to the registry. These files govern internal hve-core repository concerns (CI/CD workflows, repo-specific conventions) that do not apply outside this repository. They are excluded from: +Instructions placed in `.github/instructions/hve-core/` are repo-specific and MUST NOT be added to collection manifests. These files govern internal hve-core repository concerns (CI/CD workflows, repo-specific conventions) that do not apply outside this repository. They are excluded from: -* The AI artifacts registry +* Collection manifests * Extension packaging and distribution * Persona collection builds -* Orphan detection in registry validation +* Artifact selection for published bundles If your instructions apply only to the hve-core repository and are not intended for distribution to consumers, place them in `.github/instructions/hve-core/`. Otherwise, place them in `.github/instructions/` or a technology-specific subdirectory (e.g., `csharp/`, `bash/`). @@ -210,7 +201,7 @@ When in doubt, include `hve-core-all` to ensure the artifact appears in the full ## Dependency Declarations -Some artifacts require other artifacts to function correctly. The `requires` field in registry entries declares these dependencies explicitly. +Some artifacts require other artifacts to function correctly. Dependency behavior is resolved during packaging. ### Dependency Types @@ -291,7 +282,7 @@ When `items[].maturity` is omitted, the effective maturity defaults to `stable`. ### Default for New Contributions -New artifact registry entries **SHOULD** use `maturity: stable` unless: +New collection items **SHOULD** use `maturity: stable` unless: * The artifact is a proof-of-concept or experimental feature * The artifact requires additional testing or feedback before wide release diff --git a/docs/contributing/custom-agents.md b/docs/contributing/custom-agents.md index 9e2964a8..7fd7634b 100644 --- a/docs/contributing/custom-agents.md +++ b/docs/contributing/custom-agents.md @@ -200,26 +200,19 @@ author: 'microsoft/hve-core' --- ``` -## Registry Entry Requirements - -All agents must have a corresponding entry in `.github/ai-artifacts-registry.json`. This entry controls distribution, persona filtering, and dependency resolution. - -### Adding Your Agent to the Registry - -After creating your agent file, add an entry to the `agents` section of the registry: - -```json -"my-new-agent": { - "maturity": "stable", - "personas": ["hve-core-all", "developer"], - "tags": ["workflow", "automation"], - "requires": { - "agents": [], - "prompts": ["related-prompt"], - "instructions": ["relevant-instructions"], - "skills": [] - } -} +## Collection Entry Requirements + +All agents must have matching entries in one or more `collections/*.collection.yml` manifests. Collection entries control selection and maturity. + +### Adding Your Agent to a Collection + +After creating your agent file, add an `items[]` entry to each target collection: + +```yaml +items: + - path: .github/agents/my-new-agent.agent.md + kind: agent + maturity: stable ``` ### Selecting Personas for Agents @@ -237,18 +230,9 @@ Choose personas based on who benefits most from your agent: ### Declaring Agent Dependencies -If your agent dispatches other agents at runtime via `runSubagent`, invokes prompts, or generates code that follows specific instructions, declare these in the `requires` field. Handoff targets declared in frontmatter are resolved dynamically during packaging and should not be listed here: - -```json -"requires": { - "agents": ["task-planner"], // Agents dispatched at runtime via runSubagent - "prompts": ["task-plan"], // Prompts this agent invokes - "instructions": ["python-script"], // Instructions for generated code - "skills": [] // Skills this agent executes -} -``` +If your agent dispatches other agents at runtime via `runSubagent`, invokes prompts, or depends on skills, document those relationships in the agent content and validate packaging behavior in affected collections. -For complete registry documentation, see [AI Artifacts Common Standards - Artifact Registry](ai-artifacts-common.md#artifact-registry). +For complete collection documentation, see [AI Artifacts Common Standards - Collection Manifests](ai-artifacts-common.md#collection-manifests). ### MCP Tool Dependencies diff --git a/docs/contributing/instructions.md b/docs/contributing/instructions.md index 4507c9d9..a725080f 100644 --- a/docs/contributing/instructions.md +++ b/docs/contributing/instructions.md @@ -119,33 +119,31 @@ lastUpdated: '2025-11-19' --- ``` -## Registry Entry Requirements +## Collection Entry Requirements -All instructions must have a corresponding entry in `.github/ai-artifacts-registry.json`, except for repo-specific instructions placed in `.github/instructions/hve-core/`. This entry controls distribution and persona filtering. +All instructions must have matching entries in one or more `collections/*.collection.yml` manifests, except for repo-specific instructions placed in `.github/instructions/hve-core/`. Collection entries control distribution and maturity. > [!NOTE] -> Instructions in `.github/instructions/hve-core/` are repo-specific and MUST NOT be added to the registry. See [Repo-Specific Instructions Exclusion](ai-artifacts-common.md#repo-specific-instructions-exclusion) for details. +> Instructions in `.github/instructions/hve-core/` are repo-specific and MUST NOT be added to collection manifests. See [Repo-Specific Instructions Exclusion](ai-artifacts-common.md#repo-specific-instructions-exclusion) for details. -### Adding Your Instructions to the Registry +### Adding Your Instructions to a Collection -After creating your instructions file, add an entry to the `instructions` section of the registry: +After creating your instructions file, add an `items[]` entry in each target collection manifest: -```json -"my-language": { - "maturity": "stable", - "personas": ["hve-core-all", "developer"], - "tags": ["language"] -} +```yaml +items: + - path: .github/instructions/my-language.instructions.md + kind: instruction + maturity: stable ``` For instructions in subdirectories, use the path format: -```json -"subdirectory/my-instructions": { - "maturity": "stable", - "personas": ["hve-core-all"], - "tags": ["category"] -} +```yaml +items: + - path: .github/instructions/subdirectory/my-instructions.instructions.md + kind: instruction + maturity: stable ``` ### Selecting Personas for Instructions @@ -174,7 +172,7 @@ Common tags for instructions: | `ado` | Azure DevOps integration | | `git` | Git workflow patterns | -For complete registry documentation, see [AI Artifacts Common Standards - Artifact Registry](ai-artifacts-common.md#artifact-registry). +For complete collection documentation, see [AI Artifacts Common Standards - Collection Manifests](ai-artifacts-common.md#collection-manifests). ## Content Structure Standards diff --git a/docs/contributing/prompts.md b/docs/contributing/prompts.md index 94d4463c..42b9b045 100644 --- a/docs/contributing/prompts.md +++ b/docs/contributing/prompts.md @@ -112,20 +112,19 @@ lastUpdated: '2025-11-19' --- ``` -## Registry Entry Requirements +## Collection Entry Requirements -All prompts must have a corresponding entry in `.github/ai-artifacts-registry.json`. This entry controls distribution and persona filtering. +All prompts must have matching entries in one or more `collections/*.collection.yml` manifests. Collection entries control distribution and maturity. -### Adding Your Prompt to the Registry +### Adding Your Prompt to a Collection -After creating your prompt file, add an entry to the `prompts` section of the registry: +After creating your prompt file, add an `items[]` entry in each target collection manifest: -```json -"my-prompt": { - "maturity": "stable", - "personas": ["hve-core-all", "developer"], - "tags": ["workflow", "automation"] -} +```yaml +items: + - path: .github/prompts/my-prompt.prompt.md + kind: prompt + maturity: stable ``` ### Selecting Personas for Prompts @@ -157,7 +156,7 @@ Common tags for prompts: | `documentation` | Documentation generation | | `prompt-engineering` | Prompt building and analysis | -For complete registry documentation, see [AI Artifacts Common Standards - Artifact Registry](ai-artifacts-common.md#artifact-registry). +For complete collection documentation, see [AI Artifacts Common Standards - Collection Manifests](ai-artifacts-common.md#collection-manifests). ## Prompt Content Structure Standards @@ -464,7 +463,7 @@ Before submitting your prompt, verify: * [ ] Clear H1 title describing workflow * [ ] Overview/purpose section -* [ ] Maturity set in registry (see [Common Standards - Maturity](ai-artifacts-common.md#maturity-field-requirements)) +* [ ] Maturity set in collection item (see [Common Standards - Maturity](ai-artifacts-common.md#maturity-field-requirements)) * [ ] Prerequisites or context section * [ ] Workflow steps with clear sequence * [ ] Success criteria defined diff --git a/docs/contributing/release-process.md b/docs/contributing/release-process.md index 0f37e9c4..a15a5b15 100644 --- a/docs/contributing/release-process.md +++ b/docs/contributing/release-process.md @@ -138,7 +138,7 @@ The VS Code extension is published to two channels with different stability expe ### Maturity Levels -Each prompt, instruction, and agent has a `maturity` value in the AI Artifacts Registry (`.github/ai-artifacts-registry.json`): +Each prompt, instruction, agent, and skill can set `maturity` in `collections/*.collection.yml` under `items[]`: | Level | Description | Included In | |----------------|-------------------------------------------------|---------------------| @@ -160,11 +160,11 @@ stateDiagram-v2 ### Contributor Guidelines -* **New contributions**: Set `stable` in registry entry unless explicitly targeting early adopters -* **Experimental work**: Set `experimental` in registry entry for proof-of-concept or rapidly evolving artifacts -* **Preview promotions**: Set `preview` in registry entry when core functionality is complete -* **Stable promotions**: Set `stable` in registry entry after production validation -* **Deprecation**: Set `deprecated` in registry entry before removal to provide transition time +* **New contributions**: Set `stable` on collection items unless explicitly targeting early adopters +* **Experimental work**: Set `experimental` on collection items for proof-of-concept or rapidly evolving artifacts +* **Preview promotions**: Set `preview` on collection items when core functionality is complete +* **Stable promotions**: Set `stable` on collection items after production validation +* **Deprecation**: Set `deprecated` on collection items before removal to provide transition time --- diff --git a/docs/contributing/skills.md b/docs/contributing/skills.md index ca63e7e2..8d745dbe 100644 --- a/docs/contributing/skills.md +++ b/docs/contributing/skills.md @@ -90,20 +90,19 @@ description: 'Video-to-GIF conversion skill with FFmpeg two-pass optimization - --- ``` -## Registry Entry Requirements +## Collection Entry Requirements -All skills must have a corresponding entry in `.github/ai-artifacts-registry.json`. This entry controls distribution and persona filtering. +All skills must have matching entries in one or more `collections/*.collection.yml` manifests. Collection entries control distribution and maturity. -### Adding Your Skill to the Registry +### Adding Your Skill to a Collection -After creating your skill package, add an entry to the `skills` section of the registry: +After creating your skill package, add an `items[]` entry in each target collection manifest: -```json -"my-skill": { - "maturity": "stable", - "personas": ["hve-core-all", "developer"], - "tags": ["tooling", "media"] -} +```yaml +items: + - path: .github/skills/my-skill + kind: skill + maturity: stable ``` ### Selecting Personas for Skills @@ -129,7 +128,7 @@ Common tags for skills: | `data` | Data transformation | | `testing` | Test automation utilities | -For complete registry documentation, see [AI Artifacts Common Standards - Artifact Registry](ai-artifacts-common.md#artifact-registry). +For complete collection documentation, see [AI Artifacts Common Standards - Collection Manifests](ai-artifacts-common.md#collection-manifests). ## SKILL.md Content Structure diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index fd84c242..88ea9917 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -153,34 +153,6 @@ function Test-CollectionMaturityEligible { } } -function Get-RegistryData { - <# - .SYNOPSIS - Loads the AI artifacts registry JSON file. - .DESCRIPTION - Reads and parses the AI artifacts registry JSON file into a hashtable - containing artifact metadata keyed by type (agents, prompts, instructions, skills). - .PARAMETER RegistryPath - Path to the ai-artifacts-registry.json file. - .OUTPUTS - [hashtable] Parsed registry data with keys: agents, prompts, instructions, skills, personas, version. - #> - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$RegistryPath - ) - - if (-not (Test-Path $RegistryPath)) { - throw "AI artifacts registry not found: $RegistryPath" - } - - $content = Get-Content -Path $RegistryPath -Raw - return $content | ConvertFrom-Json -AsHashtable -} - function Get-CollectionManifest { <# .SYNOPSIS @@ -284,34 +256,13 @@ function Get-CollectionArtifactMaturity { [OutputType([string])] param( [Parameter(Mandatory = $true)] - [hashtable]$CollectionItem, - - [Parameter(Mandatory = $false)] - [hashtable]$Registry = @{} + [hashtable]$CollectionItem ) if ($CollectionItem.ContainsKey('maturity') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionItem.maturity)) { return [string]$CollectionItem.maturity } - if (-not $CollectionItem.ContainsKey('kind') -or -not $CollectionItem.ContainsKey('path')) { - return 'stable' - } - - $kind = [string]$CollectionItem.kind - $path = [string]$CollectionItem.path - if ([string]::IsNullOrWhiteSpace($kind) -or [string]::IsNullOrWhiteSpace($path)) { - return 'stable' - } - - $registryType = "${kind}s" - if ($Registry.Count -gt 0 -and $Registry.ContainsKey($registryType)) { - $artifactKey = Get-CollectionArtifactKey -Kind $kind -Path $path - if ($Registry[$registryType].ContainsKey($artifactKey) -and $Registry[$registryType][$artifactKey].ContainsKey('maturity')) { - return [string]$Registry[$registryType][$artifactKey].maturity - } - } - return 'stable' } @@ -321,14 +272,12 @@ function Get-CollectionArtifacts { Filters collection artifacts by collection item metadata and channel maturity. .DESCRIPTION Applies collection-level filtering to manifest items, returning artifact - names that match allowed maturities. Item-level maturity is preferred; - registry maturity is used as temporary fallback when item maturity is omitted. + names that match allowed maturities. Item-level maturity is used when + present; otherwise artifacts default to stable. .PARAMETER Collection Collection manifest hashtable with items. .PARAMETER AllowedMaturities Array of maturity levels to include. - .PARAMETER Registry - AI artifacts registry hashtable used as fallback for omitted item maturity. .OUTPUTS [hashtable] With Agents, Prompts, Instructions, Skills arrays of matching artifact names. #> @@ -339,10 +288,7 @@ function Get-CollectionArtifacts { [hashtable]$Collection, [Parameter(Mandatory = $true)] - [string[]]$AllowedMaturities, - - [Parameter(Mandatory = $false)] - [hashtable]$Registry = @{} + [string[]]$AllowedMaturities ) $result = @{ @@ -364,7 +310,7 @@ function Get-CollectionArtifacts { $kind = [string]$item.kind $path = [string]$item.path - $maturity = Get-CollectionArtifactMaturity -CollectionItem $item -Registry $Registry + $maturity = Get-CollectionArtifactMaturity -CollectionItem $item if ($AllowedMaturities -notcontains $maturity) { continue } @@ -393,10 +339,6 @@ function Resolve-HandoffDependencies { Initial agent names to start BFS from. .PARAMETER AgentsDir Path to the agents directory containing .agent.md files. - .PARAMETER AllowedMaturities - Array of maturity levels to include. - .PARAMETER Registry - AI artifacts registry hashtable for maturity lookup. .OUTPUTS [string[]] Complete set of agent names including seed agents and all transitive handoff targets. #> @@ -407,13 +349,7 @@ function Resolve-HandoffDependencies { [string[]]$SeedAgents, [Parameter(Mandatory = $true)] - [string]$AgentsDir, - - [Parameter(Mandatory = $true)] - [string[]]$AllowedMaturities, - - [Parameter(Mandatory = $false)] - [hashtable]$Registry = @{} + [string]$AgentsDir ) $visited = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) @@ -434,13 +370,6 @@ function Resolve-HandoffDependencies { continue } - # Check maturity from registry - $maturity = "stable" - if ($Registry.Count -gt 0 -and $Registry.ContainsKey('agents') -and $Registry.agents.ContainsKey($current)) { - $maturity = $Registry.agents[$current].maturity - } - if ($AllowedMaturities -notcontains $maturity) { continue } - # Parse handoffs from frontmatter $content = Get-Content -Path $agentFile -Raw if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') { @@ -477,20 +406,18 @@ function Resolve-HandoffDependencies { function Resolve-RequiresDependencies { <# .SYNOPSIS - Resolves transitive artifact dependencies from registry requires blocks. + Resolves transitive artifact dependencies from collection item requires blocks. .DESCRIPTION - Walks the requires blocks in agent registry entries to compute the - complete set of dependent artifacts across all types (agents, prompts, - instructions, skills) using BFS for transitive agent dependencies. + Walks requires blocks in collection items to compute the complete set of + dependent artifacts across all types (agents, prompts, instructions, skills). .PARAMETER ArtifactNames Hashtable with initial artifact name arrays keyed by type (agents, prompts, instructions, skills). - .PARAMETER Registry - AI artifacts registry hashtable. .PARAMETER AllowedMaturities Array of maturity levels to include. + .PARAMETER CollectionRequires + Per-type map of artifact requires blocks keyed by artifact name. .PARAMETER CollectionMaturities - Optional per-type maturity map keyed by artifact name. Used as - primary maturity source before registry lookup. + Optional per-type maturity map keyed by artifact name. .OUTPUTS [hashtable] With Agents, Prompts, Instructions, Skills arrays containing resolved names. #> @@ -500,12 +427,12 @@ function Resolve-RequiresDependencies { [Parameter(Mandatory = $true)] [hashtable]$ArtifactNames, - [Parameter(Mandatory = $true)] - [hashtable]$Registry, - [Parameter(Mandatory = $true)] [string[]]$AllowedMaturities, + [Parameter(Mandatory = $false)] + [hashtable]$CollectionRequires = @{}, + [Parameter(Mandatory = $false)] [hashtable]$CollectionMaturities = @{} ) @@ -534,44 +461,45 @@ function Resolve-RequiresDependencies { } } - # Walk requires for agents (only agents have requires blocks) - $processedAgents = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) - $agentQueue = [System.Collections.Generic.Queue[string]]::new() + $changed = $true + while ($changed) { + $changed = $false - foreach ($agent in $resolved.Agents) { - $agentQueue.Enqueue($agent) - } - - while ($agentQueue.Count -gt 0) { - $current = $agentQueue.Dequeue() - if (-not $processedAgents.Add($current)) { continue } - - if (-not $Registry.ContainsKey('agents') -or -not $Registry.agents.ContainsKey($current)) { continue } + foreach ($sourceType in @('agents', 'prompts', 'instructions', 'skills')) { + if (-not $CollectionRequires.ContainsKey($sourceType)) { + continue + } - $entry = $Registry.agents[$current] - if (-not $entry.ContainsKey('requires')) { continue } + $sourceCapitalType = $typeMap[$sourceType] + foreach ($sourceName in @($resolved[$sourceCapitalType])) { + if (-not $CollectionRequires[$sourceType].ContainsKey($sourceName)) { + continue + } - $requires = $entry.requires + $requires = $CollectionRequires[$sourceType][$sourceName] + if (-not $requires) { + continue + } - foreach ($type in @('agents', 'prompts', 'instructions', 'skills')) { - if (-not $requires.ContainsKey($type)) { continue } - $capitalType = $typeMap[$type] + foreach ($targetType in @('agents', 'prompts', 'instructions', 'skills')) { + if (-not $requires.ContainsKey($targetType)) { + continue + } - foreach ($dep in $requires[$type]) { - # Check maturity of dependency (collection metadata preferred, registry fallback) - $depMaturity = 'stable' - if ($CollectionMaturities.ContainsKey($type) -and $CollectionMaturities[$type].ContainsKey($dep)) { - $depMaturity = $CollectionMaturities[$type][$dep] - } - elseif ($Registry.ContainsKey($type) -and $Registry[$type].ContainsKey($dep) -and $Registry[$type][$dep].ContainsKey('maturity')) { - $depMaturity = $Registry[$type][$dep].maturity - } + $targetCapitalType = $typeMap[$targetType] + foreach ($dep in @($requires[$targetType])) { + $depMaturity = 'stable' + if ($CollectionMaturities.ContainsKey($targetType) -and $CollectionMaturities[$targetType].ContainsKey($dep)) { + $depMaturity = $CollectionMaturities[$targetType][$dep] + } - if ($AllowedMaturities -notcontains $depMaturity) { continue } + if ($AllowedMaturities -notcontains $depMaturity) { + continue + } - if ($resolved[$capitalType].Add($dep)) { - if ($type -eq 'agents') { - $agentQueue.Enqueue($dep) + if ($resolved[$targetCapitalType].Add($dep)) { + $changed = $true + } } } } @@ -645,16 +573,13 @@ function Get-DiscoveredAgents { Discovers chat agent files from the agents directory. .DESCRIPTION Discovery function that scans the agents directory for .agent.md files, - extracts frontmatter description, filters by registry maturity and exclusion list, - and returns structured agent objects. + filters by exclusion list, and returns structured agent objects. .PARAMETER AgentsDir Path to the agents directory. .PARAMETER AllowedMaturities Array of maturity levels to include. .PARAMETER ExcludedAgents Array of agent names to exclude from packaging. - .PARAMETER Registry - AI artifacts registry hashtable for maturity lookup. .OUTPUTS [hashtable] With Agents array, Skipped array, and DirectoryExists bool. #> @@ -668,10 +593,7 @@ function Get-DiscoveredAgents { [string[]]$AllowedMaturities, [Parameter(Mandatory = $false)] - [string[]]$ExcludedAgents = @(), - - [Parameter(Mandatory = $false)] - [hashtable]$Registry = @{} + [string[]]$ExcludedAgents = @() ) $result = @{ @@ -694,11 +616,7 @@ function Get-DiscoveredAgents { continue } - # Determine maturity from registry if available, else default to stable $maturity = "stable" - if ($Registry.Count -gt 0 -and $Registry.ContainsKey('agents') -and $Registry.agents.ContainsKey($agentName)) { - $maturity = $Registry.agents[$agentName].maturity - } if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $agentName; Reason = "maturity: $maturity" } @@ -720,16 +638,13 @@ function Get-DiscoveredPrompts { Discovers prompt files from the prompts directory. .DESCRIPTION Discovery function that scans the prompts directory for .prompt.md files, - extracts frontmatter description, filters by registry maturity, and returns - structured prompt objects with relative paths. + and returns structured prompt objects with relative paths. .PARAMETER PromptsDir Path to the prompts directory. .PARAMETER GitHubDir Path to the .github directory for relative path calculation. .PARAMETER AllowedMaturities Array of maturity levels to include. - .PARAMETER Registry - AI artifacts registry hashtable for maturity lookup. .OUTPUTS [hashtable] With Prompts array, Skipped array, and DirectoryExists bool. #> @@ -743,10 +658,7 @@ function Get-DiscoveredPrompts { [string]$GitHubDir, [Parameter(Mandatory = $true)] - [string[]]$AllowedMaturities, - - [Parameter(Mandatory = $false)] - [hashtable]$Registry = @{} + [string[]]$AllowedMaturities ) $result = @{ @@ -763,11 +675,7 @@ function Get-DiscoveredPrompts { foreach ($promptFile in $promptFiles) { $promptName = $promptFile.BaseName -replace '\.prompt$', '' - # Determine maturity from registry if available, else default to stable $maturity = "stable" - if ($Registry.Count -gt 0 -and $Registry.ContainsKey('prompts') -and $Registry.prompts.ContainsKey($promptName)) { - $maturity = $Registry.prompts[$promptName].maturity - } if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $promptName; Reason = "maturity: $maturity" } @@ -791,16 +699,13 @@ function Get-DiscoveredInstructions { Discovers instruction files from the instructions directory. .DESCRIPTION Discovery function that scans the instructions directory for .instructions.md files, - extracts frontmatter description, filters by registry maturity, and returns - structured instruction objects with normalized paths. + and returns structured instruction objects with normalized paths. .PARAMETER InstructionsDir Path to the instructions directory. .PARAMETER GitHubDir Path to the .github directory for relative path calculation. .PARAMETER AllowedMaturities Array of maturity levels to include. - .PARAMETER Registry - AI artifacts registry hashtable for maturity lookup. .OUTPUTS [hashtable] With Instructions array, Skipped array, and DirectoryExists bool. #> @@ -814,10 +719,7 @@ function Get-DiscoveredInstructions { [string]$GitHubDir, [Parameter(Mandatory = $true)] - [string[]]$AllowedMaturities, - - [Parameter(Mandatory = $false)] - [hashtable]$Registry = @{} + [string[]]$AllowedMaturities ) $result = @{ @@ -842,13 +744,7 @@ function Get-DiscoveredInstructions { $baseName = $instrFile.BaseName -replace '\.instructions$', '' $instrName = "$baseName-instructions" - # Determine maturity from registry using relative path key - $relPath = [System.IO.Path]::GetRelativePath($InstructionsDir, $instrFile.FullName) -replace '\\', '/' - $registryKey = $relPath -replace '\.instructions\.md$', '' $maturity = "stable" - if ($Registry.Count -gt 0 -and $Registry.ContainsKey('instructions') -and $Registry.instructions.ContainsKey($registryKey)) { - $maturity = $Registry.instructions[$registryKey].maturity - } if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $instrName; Reason = "maturity: $maturity" } @@ -873,14 +769,11 @@ function Get-DiscoveredSkills { Discovers skill packages from the skills directory. .DESCRIPTION Discovery function that scans the skills directory for subdirectories - containing SKILL.md files, filters by registry maturity, and returns - structured skill objects. + containing SKILL.md files and returns structured skill objects. .PARAMETER SkillsDir Path to the skills directory. .PARAMETER AllowedMaturities Array of maturity levels to include. - .PARAMETER Registry - AI artifacts registry hashtable for maturity lookup. .OUTPUTS [hashtable] With Skills array, Skipped array, and DirectoryExists bool. #> @@ -891,10 +784,7 @@ function Get-DiscoveredSkills { [string]$SkillsDir, [Parameter(Mandatory = $true)] - [string[]]$AllowedMaturities, - - [Parameter(Mandatory = $false)] - [hashtable]$Registry = @{} + [string[]]$AllowedMaturities ) $result = @{ @@ -919,9 +809,6 @@ function Get-DiscoveredSkills { } $maturity = "stable" - if ($Registry.Count -gt 0 -and $Registry.ContainsKey('skills') -and $Registry.skills.ContainsKey($skillName)) { - $maturity = $Registry.skills[$skillName].maturity - } if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $skillName; Reason = "maturity: $maturity" } @@ -1227,14 +1114,6 @@ function Invoke-PrepareExtension { return New-PrepareResult -Success $false -ErrorMessage "Required paths not found: $missingPaths" } - # Load AI artifacts registry if available - $registryPath = Join-Path $GitHubDir "ai-artifacts-registry.json" - $registry = @{} - if (Test-Path $registryPath) { - $registry = Get-RegistryData -RegistryPath $registryPath - Write-Host "Registry loaded: $registryPath" - } - # Read and parse package.json try { $packageJsonContent = Get-Content -Path $PackageJsonPath -Raw @@ -1270,6 +1149,7 @@ function Invoke-PrepareExtension { $collectionManifest = $null $collectionArtifactNames = $null $collectionMaturities = @{} + $collectionRequires = @{} if ($Collection -and $Collection -ne "") { $collectionManifest = Get-CollectionManifest -CollectionPath $Collection @@ -1299,6 +1179,7 @@ function Invoke-PrepareExtension { # Build collection maturity map and channel-filtered artifact names $collectionMaturities = @{} + $collectionRequires = @{} if ($artifactCollectionManifest.ContainsKey('items')) { foreach ($item in $artifactCollectionManifest.items) { @@ -1309,20 +1190,27 @@ function Invoke-PrepareExtension { $itemKind = [string]$item.kind $itemPath = [string]$item.path $artifactKey = Get-CollectionArtifactKey -Kind $itemKind -Path $itemPath - $effectiveMaturity = Get-CollectionArtifactMaturity -CollectionItem $item -Registry $registry + $effectiveMaturity = Get-CollectionArtifactMaturity -CollectionItem $item if (-not $collectionMaturities.ContainsKey("${itemKind}s") -or $null -eq $collectionMaturities["${itemKind}s"]) { $collectionMaturities["${itemKind}s"] = @{} } $collectionMaturities["${itemKind}s"][$artifactKey] = $effectiveMaturity + + if ($item.ContainsKey('requires') -and $item.requires) { + if (-not $collectionRequires.ContainsKey("${itemKind}s") -or $null -eq $collectionRequires["${itemKind}s"]) { + $collectionRequires["${itemKind}s"] = @{} + } + $collectionRequires["${itemKind}s"][$artifactKey] = $item.requires + } } } - $collectionArtifactNames = Get-CollectionArtifacts -Registry $registry -Collection $artifactCollectionManifest -AllowedMaturities $allowedMaturities + $collectionArtifactNames = Get-CollectionArtifacts -Collection $artifactCollectionManifest -AllowedMaturities $allowedMaturities # Resolve handoff dependencies (agents only) if (@($collectionArtifactNames.Agents).Count -gt 0) { $agentsDir = Join-Path $GitHubDir "agents" - $expandedAgents = Resolve-HandoffDependencies -SeedAgents $collectionArtifactNames.Agents -AgentsDir $agentsDir -AllowedMaturities $allowedMaturities -Registry $registry + $expandedAgents = Resolve-HandoffDependencies -SeedAgents $collectionArtifactNames.Agents -AgentsDir $agentsDir $collectionArtifactNames.Agents = $expandedAgents } @@ -1332,7 +1220,7 @@ function Invoke-PrepareExtension { prompts = $collectionArtifactNames.Prompts instructions = $collectionArtifactNames.Instructions skills = $collectionArtifactNames.Skills - } -Registry $registry -AllowedMaturities $allowedMaturities -CollectionMaturities $collectionMaturities + } -AllowedMaturities $allowedMaturities -CollectionRequires $collectionRequires -CollectionMaturities $collectionMaturities $collectionArtifactNames = @{ Agents = $resolvedNames.Agents @@ -1351,7 +1239,7 @@ function Invoke-PrepareExtension { } $agentsDir = Join-Path $GitHubDir "agents" - $agentResult = Get-DiscoveredAgents -AgentsDir $agentsDir -AllowedMaturities $discoveryAllowedMaturities -ExcludedAgents @() -Registry $registry + $agentResult = Get-DiscoveredAgents -AgentsDir $agentsDir -AllowedMaturities $discoveryAllowedMaturities -ExcludedAgents @() $chatAgents = $agentResult.Agents $excludedAgents = $agentResult.Skipped @@ -1363,7 +1251,7 @@ function Invoke-PrepareExtension { # Discover prompts $promptsDir = Join-Path $GitHubDir "prompts" - $promptResult = Get-DiscoveredPrompts -PromptsDir $promptsDir -GitHubDir $GitHubDir -AllowedMaturities $discoveryAllowedMaturities -Registry $registry + $promptResult = Get-DiscoveredPrompts -PromptsDir $promptsDir -GitHubDir $GitHubDir -AllowedMaturities $discoveryAllowedMaturities $chatPrompts = $promptResult.Prompts $excludedPrompts = $promptResult.Skipped @@ -1375,7 +1263,7 @@ function Invoke-PrepareExtension { # Discover instructions $instructionsDir = Join-Path $GitHubDir "instructions" - $instructionResult = Get-DiscoveredInstructions -InstructionsDir $instructionsDir -GitHubDir $GitHubDir -AllowedMaturities $discoveryAllowedMaturities -Registry $registry + $instructionResult = Get-DiscoveredInstructions -InstructionsDir $instructionsDir -GitHubDir $GitHubDir -AllowedMaturities $discoveryAllowedMaturities $chatInstructions = $instructionResult.Instructions $excludedInstructions = $instructionResult.Skipped @@ -1387,7 +1275,7 @@ function Invoke-PrepareExtension { # Discover skills $skillsDir = Join-Path $GitHubDir "skills" - $skillResult = Get-DiscoveredSkills -SkillsDir $skillsDir -AllowedMaturities $discoveryAllowedMaturities -Registry $registry + $skillResult = Get-DiscoveredSkills -SkillsDir $skillsDir -AllowedMaturities $discoveryAllowedMaturities $chatSkills = $skillResult.Skills $excludedSkills = $skillResult.Skipped diff --git a/scripts/linting/schemas/ai-artifacts-registry.schema.json b/scripts/linting/schemas/ai-artifacts-registry.schema.json deleted file mode 100644 index 7db44347..00000000 --- a/scripts/linting/schemas/ai-artifacts-registry.schema.json +++ /dev/null @@ -1,195 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/microsoft/hve-core/schemas/ai-artifacts-registry.schema.json", - "title": "AI Artifacts Registry Schema", - "description": "Schema for the centralized AI artifacts registry containing persona tags, maturity levels, dependency mappings, and metadata for all HVE-Core artifacts", - "type": "object", - "required": [ - "$schema", - "version", - "personas", - "agents", - "prompts", - "instructions", - "skills" - ], - "properties": { - "$schema": { - "type": "string", - "description": "JSON Schema reference for validation" - }, - "version": { - "type": "string", - "pattern": "^\\d+\\.\\d+$", - "description": "Registry format version (semver major.minor)" - }, - "personas": { - "type": "object", - "required": [ - "definitions" - ], - "properties": { - "definitions": { - "type": "object", - "description": "Persona ID to metadata mapping", - "additionalProperties": { - "type": "object", - "required": [ - "name", - "description" - ], - "properties": { - "name": { - "type": "string", - "minLength": 1 - }, - "description": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "agents": { - "type": "object", - "description": "Agent artifact entries keyed by agent name", - "additionalProperties": { - "$ref": "#/$defs/agentEntry" - } - }, - "prompts": { - "type": "object", - "description": "Prompt artifact entries keyed by prompt name", - "additionalProperties": { - "$ref": "#/$defs/simpleArtifactEntry" - } - }, - "instructions": { - "type": "object", - "description": "Instruction artifact entries keyed by instruction name (use dir/name for subdirectory files)", - "additionalProperties": { - "$ref": "#/$defs/simpleArtifactEntry" - } - }, - "skills": { - "type": "object", - "description": "Skill artifact entries keyed by skill directory name", - "additionalProperties": { - "$ref": "#/$defs/simpleArtifactEntry" - } - } - }, - "additionalProperties": false, - "$defs": { - "agentEntry": { - "type": "object", - "required": [ - "maturity", - "personas", - "tags" - ], - "properties": { - "maturity": { - "type": "string", - "enum": [ - "stable", - "preview", - "experimental", - "deprecated" - ], - "description": "Maturity level for channel-based filtering" - }, - "personas": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" - }, - "description": "Persona IDs this artifact belongs to. Empty array means universal (all personas)." - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Freeform categorization tags" - }, - "requires": { - "type": "object", - "properties": { - "agents": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Agent names this artifact dispatches at runtime via runSubagent. Handoff targets (frontmatter handoffs field) are resolved dynamically during packaging and excluded from this array to avoid circular dependencies." - }, - "prompts": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Prompt names this artifact depends on" - }, - "instructions": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Instruction names this artifact depends on" - }, - "skills": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Skill names this artifact depends on" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "simpleArtifactEntry": { - "type": "object", - "required": [ - "maturity", - "personas", - "tags" - ], - "properties": { - "maturity": { - "type": "string", - "enum": [ - "stable", - "preview", - "experimental", - "deprecated" - ], - "description": "Maturity level for channel-based filtering" - }, - "personas": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" - }, - "description": "Persona IDs this artifact belongs to. Empty array means universal (all personas)." - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Freeform categorization tags" - } - }, - "additionalProperties": false - } - } -} \ No newline at end of file diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index ba4e05a6..652d615d 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -159,12 +159,6 @@ description: "Preview agent" --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'preview.agent.md') - $script:mockRegistry = @{ - agents = @{ - 'stable' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - 'preview' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() } - } - } } AfterAll { @@ -172,19 +166,19 @@ description: "Preview agent" } It 'Discovers agents matching allowed maturities' { - $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @() -Registry $script:mockRegistry + $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @() $result.DirectoryExists | Should -BeTrue $result.Agents.Count | Should -Be 2 } It 'Filters agents by maturity' { - $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -ExcludedAgents @() -Registry $script:mockRegistry - $result.Agents.Count | Should -Be 1 - $result.Skipped.Count | Should -Be 1 + $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('preview') -ExcludedAgents @() + $result.Agents.Count | Should -Be 0 + $result.Skipped.Count | Should -Be 2 } It 'Excludes specified agents' { - $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @('stable') -Registry $script:mockRegistry + $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable', 'preview') -ExcludedAgents @('stable') $result.Agents.Count | Should -Be 1 } @@ -301,42 +295,6 @@ applyTo: "**/*.cs" } } -Describe 'Get-RegistryData' { - BeforeAll { - $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) - New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null - } - - AfterAll { - Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue - } - - It 'Loads registry from valid path' { - $registryFile = Join-Path $script:tempDir 'registry.json' - @{ agents = @{ 'test' = @{ maturity = 'stable' } } } | ConvertTo-Json -Depth 5 | Set-Content -Path $registryFile - - $result = Get-RegistryData -RegistryPath $registryFile - $result | Should -Not -BeNullOrEmpty - } - - It 'Throws when path does not exist' { - $nonexistent = Join-Path $script:tempDir 'nonexistent.json' - { Get-RegistryData -RegistryPath $nonexistent } | Should -Throw '*not found*' - } - - It 'Returns hashtable with expected keys' { - $registryFile = Join-Path $script:tempDir 'registry2.json' - @{ - agents = @{ 'a' = @{ maturity = 'stable' } } - prompts = @{ 'p' = @{ maturity = 'stable' } } - } | ConvertTo-Json -Depth 5 | Set-Content -Path $registryFile - - $result = Get-RegistryData -RegistryPath $registryFile - $result.Keys | Should -Contain 'agents' - $result.Keys | Should -Contain 'prompts' - } -} - Describe 'Get-DiscoveredSkills' { BeforeAll { $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) @@ -358,11 +316,6 @@ description: "Test skill" $emptySkillDir = Join-Path $script:skillsDir 'empty-skill' New-Item -ItemType Directory -Path $emptySkillDir -Force | Out-Null - $script:mockRegistry = @{ - skills = @{ - 'test-skill' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - } - } } AfterAll { @@ -370,7 +323,7 @@ description: "Test skill" } It 'Discovers skills in directory' { - $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry + $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable') $result.DirectoryExists | Should -BeTrue $result.Skills.Count | Should -Be 1 $result.Skills[0].name | Should -Be 'test-skill' @@ -383,19 +336,14 @@ description: "Test skill" $result.Skills | Should -BeNullOrEmpty } - It 'Filters skills by maturity from registry' { - $previewRegistry = @{ - skills = @{ - 'test-skill' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() } - } - } - $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable') -Registry $previewRegistry + It 'Filters skills when stable is not an allowed maturity' { + $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('preview') $result.Skills.Count | Should -Be 0 $result.Skipped.Count | Should -BeGreaterThan 0 } It 'Skips directories without SKILL.md' { - $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry + $result = Get-DiscoveredSkills -SkillsDir $script:skillsDir -AllowedMaturities @('stable') $skippedNames = $result.Skipped | ForEach-Object { $_.Name } $skippedNames | Should -Contain 'empty-skill' } @@ -473,21 +421,6 @@ Describe 'Test-GlobMatch' { } Describe 'Get-CollectionArtifacts' { - BeforeAll { - $script:registry = @{ - agents = @{ - 'dev-agent' = @{ maturity = 'stable'; personas = @('developer'); tags = @() } - 'all-agent' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - 'preview-dev' = @{ maturity = 'preview'; personas = @('developer'); tags = @() } - } - prompts = @{ - 'dev-prompt' = @{ maturity = 'stable'; personas = @('developer'); tags = @() } - } - instructions = @{} - skills = @{} - } - } - It 'Returns artifacts from collection items across supported kinds' { $collection = @{ items = @( @@ -498,7 +431,7 @@ Describe 'Get-CollectionArtifacts' { ) } - $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable', 'preview') + $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable', 'preview') $result.Agents | Should -Contain 'dev-agent' $result.Prompts | Should -Contain 'dev-prompt' $result.Instructions | Should -Contain 'dev/dev' @@ -513,12 +446,12 @@ Describe 'Get-CollectionArtifacts' { ) } - $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable') + $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable') $result.Agents | Should -Contain 'dev-agent' $result.Agents | Should -Not -Contain 'preview-dev' } - It 'Falls back to registry maturity when item maturity is omitted' { + It 'Defaults to stable maturity when item maturity is omitted' { $collection = @{ items = @( @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md' }, @@ -526,14 +459,14 @@ Describe 'Get-CollectionArtifacts' { ) } - $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable') + $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable') $result.Agents | Should -Contain 'dev-agent' - $result.Agents | Should -Not -Contain 'preview-dev' + $result.Agents | Should -Contain 'preview-dev' } It 'Returns empty when collection has no items' { $collection = @{ id = 'empty' } - $result = Get-CollectionArtifacts -Registry $script:registry -Collection $collection -AllowedMaturities @('stable') + $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable') $result.Agents.Count | Should -Be 0 $result.Prompts.Count | Should -Be 0 $result.Instructions.Count | Should -Be 0 @@ -600,16 +533,6 @@ handoffs: --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-b.agent.md') - $script:mockRegistry = @{ - agents = @{ - 'solo' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - 'parent' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - 'child' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - 'self-ref' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - 'chain-a' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - 'chain-b' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - } - } } AfterAll { @@ -617,25 +540,25 @@ handoffs: } It 'Returns seed agents when no handoffs' { - $result = Resolve-HandoffDependencies -SeedAgents @('solo') -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry + $result = Resolve-HandoffDependencies -SeedAgents @('solo') -AgentsDir $script:agentsDir $result | Should -Contain 'solo' $result.Count | Should -Be 1 } It 'Resolves single-level handoff' { - $result = Resolve-HandoffDependencies -SeedAgents @('parent') -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry + $result = Resolve-HandoffDependencies -SeedAgents @('parent') -AgentsDir $script:agentsDir $result | Should -Contain 'parent' $result | Should -Contain 'child' } It 'Handles self-referential handoffs' { - $result = Resolve-HandoffDependencies -SeedAgents @('self-ref') -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry + $result = Resolve-HandoffDependencies -SeedAgents @('self-ref') -AgentsDir $script:agentsDir $result | Should -Contain 'self-ref' $result.Count | Should -Be 1 } It 'Handles circular handoff chains' { - $result = Resolve-HandoffDependencies -SeedAgents @('chain-a') -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -Registry $script:mockRegistry + $result = Resolve-HandoffDependencies -SeedAgents @('chain-a') -AgentsDir $script:agentsDir $result | Should -Contain 'chain-a' $result | Should -Contain 'chain-b' $result.Count | Should -Be 2 @@ -644,59 +567,30 @@ handoffs: Describe 'Resolve-RequiresDependencies' { It 'Resolves agent requires to include dependent prompts' { - $registry = @{ - agents = @{ - 'main' = @{ - maturity = 'stable' - personas = @('hve-core-all') - requires = @{ prompts = @('dep-prompt') } - } - } - prompts = @{ - 'dep-prompt' = @{ maturity = 'stable'; personas = @('hve-core-all') } - } - } - $result = Resolve-RequiresDependencies -ArtifactNames @{ agents = @('main') } -Registry $registry -AllowedMaturities @('stable') + $result = Resolve-RequiresDependencies ` + -ArtifactNames @{ agents = @('main') } ` + -AllowedMaturities @('stable') ` + -CollectionRequires @{ agents = @{ 'main' = @{ prompts = @('dep-prompt') } } } ` + -CollectionMaturities @{ prompts = @{ 'dep-prompt' = 'stable' } } $result.Prompts | Should -Contain 'dep-prompt' } It 'Resolves transitive agent dependencies' { - $registry = @{ - agents = @{ - 'top' = @{ - maturity = 'stable' - personas = @('hve-core-all') - requires = @{ agents = @('mid') } - } - 'mid' = @{ - maturity = 'stable' - personas = @('hve-core-all') - requires = @{ prompts = @('leaf-prompt') } - } - } - prompts = @{ - 'leaf-prompt' = @{ maturity = 'stable'; personas = @('hve-core-all') } - } - } - $result = Resolve-RequiresDependencies -ArtifactNames @{ agents = @('top') } -Registry $registry -AllowedMaturities @('stable') + $result = Resolve-RequiresDependencies ` + -ArtifactNames @{ agents = @('top') } ` + -AllowedMaturities @('stable') ` + -CollectionRequires @{ agents = @{ 'top' = @{ agents = @('mid') }; 'mid' = @{ prompts = @('leaf-prompt') } } } ` + -CollectionMaturities @{ agents = @{ 'mid' = 'stable' }; prompts = @{ 'leaf-prompt' = 'stable' } } $result.Agents | Should -Contain 'mid' $result.Prompts | Should -Contain 'leaf-prompt' } It 'Respects maturity filter on dependencies' { - $registry = @{ - agents = @{ - 'main' = @{ - maturity = 'stable' - personas = @('hve-core-all') - requires = @{ prompts = @('exp-prompt') } - } - } - prompts = @{ - 'exp-prompt' = @{ maturity = 'experimental'; personas = @('hve-core-all') } - } - } - $result = Resolve-RequiresDependencies -ArtifactNames @{ agents = @('main') } -Registry $registry -AllowedMaturities @('stable') + $result = Resolve-RequiresDependencies ` + -ArtifactNames @{ agents = @('main') } ` + -AllowedMaturities @('stable') ` + -CollectionRequires @{ agents = @{ 'main' = @{ prompts = @('exp-prompt') } } } ` + -CollectionMaturities @{ prompts = @{ 'exp-prompt' = 'experimental' } } $result.Prompts | Should -Not -Contain 'exp-prompt' } } @@ -815,22 +709,6 @@ applyTo: "**/*.ps1" # Instruction '@ | Set-Content -Path (Join-Path $script:instrDir 'test.instructions.md') - # Create mock registry for all Invoke-PrepareExtension tests - $registryContent = @{ - version = "1.0" - personas = @{ definitions = @{ 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } } } - agents = @{ - 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - } - prompts = @{ - 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - } - instructions = @{ - 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - } - skills = @{} - } - $registryContent | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:ghDir 'ai-artifacts-registry.json') } AfterAll { @@ -870,34 +748,31 @@ description: "Preview agent" --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'preview.agent.md') - # Update registry with preview agent - $registryContent = @{ - version = "1.0" - personas = @{ definitions = @{ 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } } } - agents = @{ - 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - 'preview' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() } - } - prompts = @{ - 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - } - instructions = @{ - 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - } - skills = @{} - } - $registryContent | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:ghDir 'ai-artifacts-registry.json') + $collectionPath = Join-Path $script:tempDir 'channel-filter.collection.json' + @{ + id = 'hve-core-all' + name = 'hve-core-all' + displayName = 'HVE Core - All' + description = 'Channel filtering test' + personas = @('hve-core-all') + items = @( + @{ kind = 'agent'; path = '.github/agents/test.agent.md'; maturity = 'stable' }, + @{ kind = 'agent'; path = '.github/agents/preview.agent.md'; maturity = 'preview' } + ) + } | ConvertTo-Json -Depth 8 | Set-Content -Path $collectionPath $stableResult = Invoke-PrepareExtension ` -ExtensionDirectory $script:extDir ` -RepoRoot $script:tempDir ` -Channel 'Stable' ` + -Collection $collectionPath ` -DryRun $preReleaseResult = Invoke-PrepareExtension ` -ExtensionDirectory $script:extDir ` -RepoRoot $script:tempDir ` -Channel 'PreRelease' ` + -Collection $collectionPath ` -DryRun $preReleaseResult.AgentCount | Should -BeGreaterThan $stableResult.AgentCount @@ -919,36 +794,34 @@ applyTo: "**/*.js" --- '@ | Set-Content -Path (Join-Path $script:instrDir 'preview.instructions.md') - # Update registry with all artifacts - $registryContent = @{ - version = "1.0" - personas = @{ definitions = @{ 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } } } - agents = @{ - 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - 'preview' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() } - } - prompts = @{ - 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - 'experimental' = @{ maturity = 'experimental'; personas = @('hve-core-all'); tags = @() } - } - instructions = @{ - 'test' = @{ maturity = 'stable'; personas = @('hve-core-all'); tags = @() } - 'preview' = @{ maturity = 'preview'; personas = @('hve-core-all'); tags = @() } - } - skills = @{} - } - $registryContent | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:ghDir 'ai-artifacts-registry.json') + $collectionPath = Join-Path $script:tempDir 'prompt-instruction-filter.collection.json' + @{ + id = 'hve-core-all' + name = 'hve-core-all' + displayName = 'HVE Core - All' + description = 'Prompt/instruction filtering test' + personas = @('hve-core-all') + items = @( + @{ kind = 'agent'; path = '.github/agents/test.agent.md'; maturity = 'stable' }, + @{ kind = 'prompt'; path = '.github/prompts/test.prompt.md'; maturity = 'stable' }, + @{ kind = 'prompt'; path = '.github/prompts/experimental.prompt.md'; maturity = 'experimental' }, + @{ kind = 'instruction'; path = '.github/instructions/test.instructions.md'; maturity = 'stable' }, + @{ kind = 'instruction'; path = '.github/instructions/preview.instructions.md'; maturity = 'preview' } + ) + } | ConvertTo-Json -Depth 8 | Set-Content -Path $collectionPath $stableResult = Invoke-PrepareExtension ` -ExtensionDirectory $script:extDir ` -RepoRoot $script:tempDir ` -Channel 'Stable' ` + -Collection $collectionPath ` -DryRun $preReleaseResult = Invoke-PrepareExtension ` -ExtensionDirectory $script:extDir ` -RepoRoot $script:tempDir ` -Channel 'PreRelease' ` + -Collection $collectionPath ` -DryRun $preReleaseResult.PromptCount | Should -BeGreaterThan $stableResult.PromptCount @@ -1078,28 +951,6 @@ applyTo: "**/*.js" } '@ | Set-Content -Path (Join-Path $script:extDir 'package.developer.json') - # Update registry with developer and nonexistent persona entries - $registryContent = @{ - version = '1.0' - personas = @{ - definitions = @{ - 'hve-core-all' = @{ name = 'All'; description = 'All artifacts' } - 'developer' = @{ name = 'Developer'; description = 'Developer artifacts' } - 'nonexistent' = @{ name = 'Nonexistent'; description = 'Missing template' } - } - } - agents = @{ - 'test' = @{ maturity = 'stable'; personas = @('hve-core-all', 'developer', 'nonexistent'); tags = @() } - } - prompts = @{ - 'test' = @{ maturity = 'stable'; personas = @('hve-core-all', 'developer', 'nonexistent'); tags = @() } - } - instructions = @{ - 'test' = @{ maturity = 'stable'; personas = @('hve-core-all', 'developer', 'nonexistent'); tags = @() } - } - skills = @{} - } - $registryContent | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $script:ghDir 'ai-artifacts-registry.json') } BeforeEach { From a861f8a748dab515c469f951b64fddbf5de18b0e Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 19:45:19 -0800 Subject: [PATCH 45/62] style(settings): update search.followSymlinks setting for clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ”ง - Generated by Copilot --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a87446e..32fe0aed 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "[markdown]": { "editor.defaultFormatter": "davidanson.vscode-markdownlint" }, + "search.followSymlinks": false, "markdown.mermaid.theme": "default", "chat.instructionsFilesLocations": { ".github/instructions/**/*.instructions.md": true, From 2d04347d5718eda95567fb807069472123f47fcc Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 19:59:38 -0800 Subject: [PATCH 46/62] style(prompts): adjust formatting for Copilot skill entry in pull request prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ”ง - Generated by Copilot --- .github/prompts/pull-request.prompt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/prompts/pull-request.prompt.md b/.github/prompts/pull-request.prompt.md index 76b8b00d..8367ec18 100644 --- a/.github/prompts/pull-request.prompt.md +++ b/.github/prompts/pull-request.prompt.md @@ -134,7 +134,7 @@ Analyze changed files from the `` section of `pr-reference.xml` and e | Copilot instructions | `.*\.instructions\.md$` | N/A | N/A | | Copilot prompt | `.*\.prompt\.md$` | N/A | N/A | | Copilot agent | `.*\.agent\.md$` | N/A | N/A | -| Copilot skill | `.*/SKILL\.md$` | N/A | N/A | +| Copilot skill | `.*/SKILL\.md$` | N/A | N/A | | Script or automation | `.*\.(ps1\|sh\|py)$` | N/A | N/A | Priority rules: From feb4b7ee7f64448f2793aab62ba72ea6dc54239f Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 20:40:16 -0800 Subject: [PATCH 47/62] feat(collections): add maturity levels and exclude repo-specific artifacts from collections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - introduce maturity levels for agents, prompts, and instructions - enforce exclusion of artifacts under .github/**/hve-core/ from collection manifests - add tests for path exclusions and maturity resolution ๐Ÿ”’ - Generated by Copilot --- .github/copilot-instructions.md | 1 + collections/github.collection.yml | 11 + collections/hve-core-all.collection.yml | 14 +- scripts/plugins/Modules/PluginHelpers.psm1 | 6 + scripts/plugins/Validate-Collections.ps1 | 5 + scripts/tests/plugins/PluginHelpers.Tests.ps1 | 167 +++++++++++++++ .../plugins/Validate-Collections.Tests.ps1 | 199 ++++++++++++++++++ 7 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 scripts/tests/plugins/PluginHelpers.Tests.ps1 create mode 100644 scripts/tests/plugins/Validate-Collections.Tests.ps1 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 21ad4108..f4ec8238 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -92,6 +92,7 @@ All tracking files use markdown format with frontmatter and follow patterns from * Scripts used by the codebase have an `npm run` script for ease of use. * Files under the root `plugins/` directory are generated outputs and are not edited directly. * Regenerate plugin outputs using `npm run plugin:generate`; markdown files under `plugins/` can be symlinked or generated, so direct edits can cause conflicts and non-durable changes. +* Artifacts under `.github/**/hve-core/` are repo-specific and excluded from collection manifests, plugin generation, and extension packaging. Validation enforces this rule. PowerShell scripts follow PSScriptAnalyzer rules from `PSScriptAnalyzer.psd1` and include proper comment-based help. Validation runs via `npm run lint:ps` with results output to `logs/`. diff --git a/collections/github.collection.yml b/collections/github.collection.yml index eab88350..8f088ff8 100644 --- a/collections/github.collection.yml +++ b/collections/github.collection.yml @@ -11,6 +11,7 @@ items: # Agents - path: .github/agents/github-backlog-manager.agent.md kind: agent + maturity: experimental # RPI agents - path: .github/agents/rpi-agent.agent.md kind: agent @@ -30,14 +31,19 @@ items: # Prompts - path: .github/prompts/github-add-issue.prompt.md kind: prompt + maturity: experimental - path: .github/prompts/github-discover-issues.prompt.md kind: prompt + maturity: experimental - path: .github/prompts/github-triage-issues.prompt.md kind: prompt + maturity: experimental - path: .github/prompts/github-execute-backlog.prompt.md kind: prompt + maturity: experimental - path: .github/prompts/github-sprint-plan.prompt.md kind: prompt + maturity: experimental # RPI prompts - path: .github/prompts/rpi.prompt.md kind: prompt @@ -71,13 +77,18 @@ items: # Bundle-specific instructions - path: .github/instructions/github-backlog-discovery.instructions.md kind: instruction + maturity: experimental - path: .github/instructions/github-backlog-planning.instructions.md kind: instruction + maturity: experimental - path: .github/instructions/github-backlog-triage.instructions.md kind: instruction + maturity: experimental - path: .github/instructions/github-backlog-update.instructions.md kind: instruction + maturity: experimental - path: .github/instructions/community-interaction.instructions.md kind: instruction + maturity: experimental display: ordering: manual diff --git a/collections/hve-core-all.collection.yml b/collections/hve-core-all.collection.yml index 9cdcb52e..1a12eb4f 100644 --- a/collections/hve-core-all.collection.yml +++ b/collections/hve-core-all.collection.yml @@ -24,8 +24,10 @@ items: kind: agent - path: .github/agents/github-backlog-manager.agent.md kind: agent + maturity: experimental - path: .github/agents/github-issue-manager.agent.md kind: agent + maturity: deprecated - path: .github/agents/hve-core-installer.agent.md kind: agent - path: .github/agents/memory.agent.md @@ -74,14 +76,19 @@ items: kind: prompt - path: .github/prompts/github-add-issue.prompt.md kind: prompt + maturity: experimental - path: .github/prompts/github-discover-issues.prompt.md kind: prompt + maturity: experimental - path: .github/prompts/github-execute-backlog.prompt.md kind: prompt + maturity: experimental - path: .github/prompts/github-sprint-plan.prompt.md kind: prompt + maturity: experimental - path: .github/prompts/github-triage-issues.prompt.md kind: prompt + maturity: experimental - path: .github/prompts/incident-response.prompt.md kind: prompt - path: .github/prompts/prompt-analyze.prompt.md @@ -122,6 +129,7 @@ items: kind: instruction - path: .github/instructions/community-interaction.instructions.md kind: instruction + maturity: experimental - path: .github/instructions/csharp/csharp-tests.instructions.md kind: instruction - path: .github/instructions/csharp/csharp.instructions.md @@ -130,16 +138,18 @@ items: kind: instruction - path: .github/instructions/github-backlog-discovery.instructions.md kind: instruction + maturity: experimental - path: .github/instructions/github-backlog-planning.instructions.md kind: instruction + maturity: experimental - path: .github/instructions/github-backlog-triage.instructions.md kind: instruction + maturity: experimental - path: .github/instructions/github-backlog-update.instructions.md kind: instruction + maturity: experimental - path: .github/instructions/hve-core-location.instructions.md kind: instruction -- path: .github/instructions/hve-core/workflows.instructions.md - kind: instruction - path: .github/instructions/markdown.instructions.md kind: instruction - path: .github/instructions/prompt-builder.instructions.md diff --git a/scripts/plugins/Modules/PluginHelpers.psm1 b/scripts/plugins/Modules/PluginHelpers.psm1 index b3d1f95f..0d388e45 100644 --- a/scripts/plugins/Modules/PluginHelpers.psm1 +++ b/scripts/plugins/Modules/PluginHelpers.psm1 @@ -196,6 +196,12 @@ function Get-ArtifactFiles { $suffix = $Matches['suffix'].ToLowerInvariant() $kind = if ($suffixToKind.ContainsKey($suffix)) { $suffixToKind[$suffix] } else { $suffix } $relativePath = [System.IO.Path]::GetRelativePath($RepoRoot, $file.FullName) -replace '\\', '/' + + # Exclude repo-specific artifacts under .github/**/hve-core/ + if ($relativePath -match '^\.github/.*/hve-core/') { + continue + } + $items += @{ path = $relativePath; kind = $kind } } } diff --git a/scripts/plugins/Validate-Collections.ps1 b/scripts/plugins/Validate-Collections.ps1 index 36422e45..b4454c69 100644 --- a/scripts/plugins/Validate-Collections.ps1 +++ b/scripts/plugins/Validate-Collections.ps1 @@ -242,6 +242,11 @@ function Invoke-CollectionValidation { } $effectiveMaturity = Resolve-ItemMaturity -Maturity $itemMaturity + # Repo-specific path exclusion + if ($itemPath -match '^\.github/.*/hve-core/') { + $fileErrors += "repo-specific path not allowed in collections: $itemPath (artifacts under .github/**/hve-core/ are excluded from distribution)" + } + # Path existence if (-not (Test-Path -Path $absolutePath)) { $fileErrors += "path not found: $itemPath" diff --git a/scripts/tests/plugins/PluginHelpers.Tests.ps1 b/scripts/tests/plugins/PluginHelpers.Tests.ps1 new file mode 100644 index 00000000..377d8200 --- /dev/null +++ b/scripts/tests/plugins/PluginHelpers.Tests.ps1 @@ -0,0 +1,167 @@ +#Requires -Modules Pester +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: MIT + +BeforeAll { + Import-Module $PSScriptRoot/../../plugins/Modules/PluginHelpers.psm1 -Force +} + +Describe 'Get-ArtifactFiles - hve-core path exclusion' { + BeforeAll { + $script:repoRoot = Join-Path $TestDrive 'repo' + $ghDir = Join-Path $script:repoRoot '.github' + + # Create agent files + $agentsDir = Join-Path $ghDir 'agents' + New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null + Set-Content -Path (Join-Path $agentsDir 'good.agent.md') -Value '---\ndescription: good\n---' + + # Create instruction files (shared) + $instrDir = Join-Path $ghDir 'instructions' + New-Item -ItemType Directory -Path $instrDir -Force | Out-Null + Set-Content -Path (Join-Path $instrDir 'shared.instructions.md') -Value '---\ndescription: shared\n---' + + # Create repo-specific files under .github/instructions/hve-core/ + $hveCoreInstrDir = Join-Path $instrDir 'hve-core' + New-Item -ItemType Directory -Path $hveCoreInstrDir -Force | Out-Null + Set-Content -Path (Join-Path $hveCoreInstrDir 'workflows.instructions.md') -Value '---\ndescription: repo-specific\n---' + + # Create repo-specific files under .github/agents/hve-core/ + $hveCoreAgentsDir = Join-Path $agentsDir 'hve-core' + New-Item -ItemType Directory -Path $hveCoreAgentsDir -Force | Out-Null + Set-Content -Path (Join-Path $hveCoreAgentsDir 'internal.agent.md') -Value '---\ndescription: repo-specific agent\n---' + + # Create a prompt file + $promptsDir = Join-Path $ghDir 'prompts' + New-Item -ItemType Directory -Path $promptsDir -Force | Out-Null + Set-Content -Path (Join-Path $promptsDir 'gen-plan.prompt.md') -Value '---\ndescription: prompt\n---' + } + + It 'Excludes files under .github/instructions/hve-core/' { + $items = Get-ArtifactFiles -RepoRoot $script:repoRoot + $paths = $items | ForEach-Object { $_.path } + $paths | Should -Not -Contain '.github/instructions/hve-core/workflows.instructions.md' + } + + It 'Excludes files under .github/agents/hve-core/' { + $items = Get-ArtifactFiles -RepoRoot $script:repoRoot + $paths = $items | ForEach-Object { $_.path } + $paths | Should -Not -Contain '.github/agents/hve-core/internal.agent.md' + } + + It 'Includes shared instruction files' { + $items = Get-ArtifactFiles -RepoRoot $script:repoRoot + $paths = $items | ForEach-Object { $_.path } + $paths | Should -Contain '.github/instructions/shared.instructions.md' + } + + It 'Includes non-hve-core agent files' { + $items = Get-ArtifactFiles -RepoRoot $script:repoRoot + $paths = $items | ForEach-Object { $_.path } + $paths | Should -Contain '.github/agents/good.agent.md' + } + + It 'Includes prompt files' { + $items = Get-ArtifactFiles -RepoRoot $script:repoRoot + $paths = $items | ForEach-Object { $_.path } + $paths | Should -Contain '.github/prompts/gen-plan.prompt.md' + } +} + +Describe 'Resolve-CollectionItemMaturity' { + It 'Returns stable for null' { + $result = Resolve-CollectionItemMaturity -Maturity $null + $result | Should -Be 'stable' + } + + It 'Returns stable for empty string' { + $result = Resolve-CollectionItemMaturity -Maturity '' + $result | Should -Be 'stable' + } + + It 'Returns stable for whitespace' { + $result = Resolve-CollectionItemMaturity -Maturity ' ' + $result | Should -Be 'stable' + } + + It 'Passes through preview' { + $result = Resolve-CollectionItemMaturity -Maturity 'preview' + $result | Should -Be 'preview' + } + + It 'Passes through experimental' { + $result = Resolve-CollectionItemMaturity -Maturity 'experimental' + $result | Should -Be 'experimental' + } +} + +Describe 'Test-ArtifactDeprecated' { + It 'Returns true for deprecated' { + $result = Test-ArtifactDeprecated -Maturity 'deprecated' + $result | Should -BeTrue + } + + It 'Returns false for stable' { + $result = Test-ArtifactDeprecated -Maturity 'stable' + $result | Should -BeFalse + } + + It 'Returns false for preview' { + $result = Test-ArtifactDeprecated -Maturity 'preview' + $result | Should -BeFalse + } + + It 'Returns false for experimental' { + $result = Test-ArtifactDeprecated -Maturity 'experimental' + $result | Should -BeFalse + } + + It 'Returns false for null (defaults to stable)' { + $result = Test-ArtifactDeprecated -Maturity $null + $result | Should -BeFalse + } +} + +Describe 'Get-PluginItemName' { + It 'Strips .agent.md suffix' { + $result = Get-PluginItemName -FileName 'task-researcher.agent.md' -Kind 'agent' + $result | Should -Be 'task-researcher.md' + } + + It 'Strips .prompt.md suffix' { + $result = Get-PluginItemName -FileName 'gen-plan.prompt.md' -Kind 'prompt' + $result | Should -Be 'gen-plan.md' + } + + It 'Strips .instructions.md suffix' { + $result = Get-PluginItemName -FileName 'csharp.instructions.md' -Kind 'instruction' + $result | Should -Be 'csharp.md' + } + + It 'Returns skill directory name unchanged' { + $result = Get-PluginItemName -FileName 'video-to-gif' -Kind 'skill' + $result | Should -Be 'video-to-gif' + } +} + +Describe 'Get-PluginSubdirectory' { + It 'Maps agent to agents' { + $result = Get-PluginSubdirectory -Kind 'agent' + $result | Should -Be 'agents' + } + + It 'Maps prompt to commands' { + $result = Get-PluginSubdirectory -Kind 'prompt' + $result | Should -Be 'commands' + } + + It 'Maps instruction to instructions' { + $result = Get-PluginSubdirectory -Kind 'instruction' + $result | Should -Be 'instructions' + } + + It 'Maps skill to skills' { + $result = Get-PluginSubdirectory -Kind 'skill' + $result | Should -Be 'skills' + } +} diff --git a/scripts/tests/plugins/Validate-Collections.Tests.ps1 b/scripts/tests/plugins/Validate-Collections.Tests.ps1 new file mode 100644 index 00000000..288305a8 --- /dev/null +++ b/scripts/tests/plugins/Validate-Collections.Tests.ps1 @@ -0,0 +1,199 @@ +#Requires -Modules Pester +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: MIT + +BeforeAll { + . $PSScriptRoot/../../plugins/Validate-Collections.ps1 +} + +Describe 'Test-KindSuffix' { + It 'Returns empty for valid agent path' { + $result = Test-KindSuffix -Kind 'agent' -ItemPath '.github/agents/rpi-agent.agent.md' -RepoRoot $TestDrive + $result | Should -BeNullOrEmpty + } + + It 'Returns empty for valid prompt path' { + $result = Test-KindSuffix -Kind 'prompt' -ItemPath '.github/prompts/gen-plan.prompt.md' -RepoRoot $TestDrive + $result | Should -BeNullOrEmpty + } + + It 'Returns empty for valid instruction path' { + $result = Test-KindSuffix -Kind 'instruction' -ItemPath '.github/instructions/csharp.instructions.md' -RepoRoot $TestDrive + $result | Should -BeNullOrEmpty + } + + It 'Returns empty for valid skill path with SKILL.md' { + $skillDir = Join-Path $TestDrive '.github/skills/video-to-gif' + New-Item -ItemType Directory -Path $skillDir -Force | Out-Null + Set-Content -Path (Join-Path $skillDir 'SKILL.md') -Value '# Skill' + + $result = Test-KindSuffix -Kind 'skill' -ItemPath '.github/skills/video-to-gif' -RepoRoot $TestDrive + $result | Should -BeNullOrEmpty + } + + It 'Returns error for invalid agent suffix' { + $result = Test-KindSuffix -Kind 'agent' -ItemPath '.github/agents/bad.prompt.md' -RepoRoot $TestDrive + $result | Should -Match "kind 'agent' expects" + } + + It 'Returns error for invalid prompt suffix' { + $result = Test-KindSuffix -Kind 'prompt' -ItemPath '.github/prompts/bad.agent.md' -RepoRoot $TestDrive + $result | Should -Match "kind 'prompt' expects" + } + + It 'Returns error when SKILL.md missing for skill kind' { + $emptySkillDir = Join-Path $TestDrive '.github/skills/no-skill' + New-Item -ItemType Directory -Path $emptySkillDir -Force | Out-Null + + $result = Test-KindSuffix -Kind 'skill' -ItemPath '.github/skills/no-skill' -RepoRoot $TestDrive + $result | Should -Match "kind 'skill' expects SKILL.md" + } +} + +Describe 'Resolve-ItemMaturity' { + It 'Returns stable for null maturity' { + $result = Resolve-ItemMaturity -Maturity $null + $result | Should -Be 'stable' + } + + It 'Returns stable for empty string' { + $result = Resolve-ItemMaturity -Maturity '' + $result | Should -Be 'stable' + } + + It 'Returns stable for whitespace' { + $result = Resolve-ItemMaturity -Maturity ' ' + $result | Should -Be 'stable' + } + + It 'Passes through explicit value' { + $result = Resolve-ItemMaturity -Maturity 'preview' + $result | Should -Be 'preview' + } + + It 'Passes through experimental value' { + $result = Resolve-ItemMaturity -Maturity 'experimental' + $result | Should -Be 'experimental' + } +} + +Describe 'Get-CollectionItemKey' { + It 'Builds correct composite key' { + $result = Get-CollectionItemKey -Kind 'agent' -ItemPath '.github/agents/rpi-agent.agent.md' + $result | Should -Be 'agent|.github/agents/rpi-agent.agent.md' + } + + It 'Builds key for instruction kind' { + $result = Get-CollectionItemKey -Kind 'instruction' -ItemPath '.github/instructions/csharp.instructions.md' + $result | Should -Be 'instruction|.github/instructions/csharp.instructions.md' + } +} + +Describe 'Invoke-CollectionValidation - repo-specific path rejection' { + BeforeAll { + Import-Module PowerShell-Yaml -ErrorAction Stop + + $script:repoRoot = Join-Path $TestDrive 'repo' + $script:collectionsDir = Join-Path $script:repoRoot 'collections' + + # Create artifact directories and files referenced by test collections + $instrDir = Join-Path $script:repoRoot '.github/instructions' + $hveCoreInstrDir = Join-Path $instrDir 'hve-core' + $agentsDir = Join-Path $script:repoRoot '.github/agents' + $hveCoreAgentsDir = Join-Path $agentsDir 'hve-core' + + New-Item -ItemType Directory -Path $hveCoreInstrDir -Force | Out-Null + New-Item -ItemType Directory -Path $hveCoreAgentsDir -Force | Out-Null + + Set-Content -Path (Join-Path $hveCoreInstrDir 'workflows.instructions.md') -Value '---\ndescription: repo-specific\n---' + Set-Content -Path (Join-Path $instrDir 'hve-core-location.instructions.md') -Value '---\ndescription: shared\n---' + Set-Content -Path (Join-Path $hveCoreAgentsDir 'some.agent.md') -Value '---\ndescription: repo-specific agent\n---' + Set-Content -Path (Join-Path $agentsDir 'good.agent.md') -Value '---\ndescription: good agent\n---' + } + + BeforeEach { + # Clear collection files between tests to prevent cross-contamination + if (Test-Path $script:collectionsDir) { + Remove-Item -Path $script:collectionsDir -Recurse -Force + } + New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null + } + + It 'Fails validation for instruction under .github/instructions/hve-core/' { + $manifest = [ordered]@{ + id = 'test-reject-instr' + name = 'Test Reject Instruction' + description = 'Tests repo-specific instruction rejection' + items = @( + [ordered]@{ + path = '.github/instructions/hve-core/workflows.instructions.md' + kind = 'instruction' + } + ) + } + $yaml = ConvertTo-Yaml -Data $manifest + Set-Content -Path (Join-Path $script:collectionsDir 'test-reject-instr.collection.yml') -Value $yaml + + $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot + $result.Success | Should -BeFalse + $result.ErrorCount | Should -BeGreaterOrEqual 1 + } + + It 'Does NOT reject hve-core-location.instructions.md (not under hve-core/ subdirectory)' { + $manifest = [ordered]@{ + id = 'test-allow-location' + name = 'Test Allow Location' + description = 'Tests that hve-core-location is allowed' + items = @( + [ordered]@{ + path = '.github/instructions/hve-core-location.instructions.md' + kind = 'instruction' + } + ) + } + $yaml = ConvertTo-Yaml -Data $manifest + Set-Content -Path (Join-Path $script:collectionsDir 'test-allow-location.collection.yml') -Value $yaml + + $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot + $result.Success | Should -BeTrue + } + + It 'Fails validation for agent under .github/agents/hve-core/' { + $manifest = [ordered]@{ + id = 'test-reject-agent' + name = 'Test Reject Agent' + description = 'Tests repo-specific agent rejection' + items = @( + [ordered]@{ + path = '.github/agents/hve-core/some.agent.md' + kind = 'agent' + } + ) + } + $yaml = ConvertTo-Yaml -Data $manifest + Set-Content -Path (Join-Path $script:collectionsDir 'test-reject-agent.collection.yml') -Value $yaml + + $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot + $result.Success | Should -BeFalse + $result.ErrorCount | Should -BeGreaterOrEqual 1 + } + + It 'Passes validation for agent NOT under hve-core/ subdirectory' { + $manifest = [ordered]@{ + id = 'test-allow-agent' + name = 'Test Allow Agent' + description = 'Tests that normal agents pass' + items = @( + [ordered]@{ + path = '.github/agents/good.agent.md' + kind = 'agent' + } + ) + } + $yaml = ConvertTo-Yaml -Data $manifest + Set-Content -Path (Join-Path $script:collectionsDir 'test-allow-agent.collection.yml') -Value $yaml + + $result = Invoke-CollectionValidation -RepoRoot $script:repoRoot + $result.Success | Should -BeTrue + } +} From 413e92f32c9062a4e04f34c18233f87b2c5aa68b Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 21:36:48 -0800 Subject: [PATCH 48/62] refactor(extension): migrate collection manifests from JSON to YAML format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - update collection manifest paths in scripts and documentation - remove deprecated JSON collection files - adjust packaging scripts to support YAML format - modify tests to validate new YAML collection structure ๐Ÿ”„ - Generated by Copilot --- .github/workflows/extension-package.yml | 75 ++++--- .gitignore | 3 + docs/architecture/workflows.md | 2 +- docs/contributing/release-process.md | 2 +- extension/PACKAGING.md | 112 +++++----- .../collections/developer.collection.json | 11 - .../collections/hve-core-all.collection.json | 11 - extension/hve-core-2.2.0.zip | Bin 0 -> 300928 bytes extension/package.developer.json | 24 --- extension/package.json | 24 --- extension/templates/collection.template.json | 8 - release-please-config.json | 7 +- .../Generate-ExtensionCollections.ps1 | 50 +---- scripts/extension/Package-Extension.ps1 | 17 +- scripts/extension/Prepare-Extension.ps1 | 21 +- .../extension/Package-Extension.Tests.ps1 | 41 +++- .../extension/Prepare-Extension.Tests.ps1 | 195 +++++++++++------- 17 files changed, 276 insertions(+), 327 deletions(-) delete mode 100644 extension/collections/developer.collection.json delete mode 100644 extension/collections/hve-core-all.collection.json create mode 100644 extension/hve-core-2.2.0.zip delete mode 100644 extension/package.developer.json delete mode 100644 extension/package.json delete mode 100644 extension/templates/collection.template.json diff --git a/.github/workflows/extension-package.yml b/.github/workflows/extension-package.yml index 9b446724..8f8beda1 100644 --- a/.github/workflows/extension-package.yml +++ b/.github/workflows/extension-package.yml @@ -54,11 +54,6 @@ jobs: run: | Install-Module -Name PowerShell-Yaml -Force -Scope CurrentUser - - name: Generate extension collections and package templates - shell: pwsh - run: | - ./scripts/extension/Generate-ExtensionCollections.ps1 - - name: Resolve effective version id: read-version shell: bash @@ -66,7 +61,7 @@ jobs: version="${{ inputs.version }}" dev_patch="${{ inputs.dev-patch-number }}" if [ -z "$version" ]; then - version=$(jq -r .version extension/package.json) + version=$(jq -r .version extension/templates/package.template.json) fi if [ -n "$dev_patch" ]; then version="${version}-dev.${dev_patch}" @@ -75,43 +70,50 @@ jobs: - name: Discover collection manifests id: discover - shell: bash + shell: pwsh run: | - collections_dir="extension/collections" - channel="${{ inputs.channel }}" - if [ -z "$channel" ]; then - channel="Stable" - fi + Import-Module PowerShell-Yaml -ErrorAction Stop + + $collectionsDir = 'collections' + $channel = '${{ inputs.channel }}'.Trim() + if (-not $channel) { $channel = 'Stable' } - if [ ! -d "$collections_dir" ]; then - echo "::error::Collections directory not found: $collections_dir" + if (-not (Test-Path $collectionsDir)) { + Write-Error "Collections directory not found: $collectionsDir" exit 1 - fi + } - # Build JSON array of collection manifests, filtering by maturity - matrix_json=$(find "$collections_dir" -name '*.collection.json' -type f | sort | while IFS= read -r file; do - id=$(jq -r '.id' "$file") - name=$(jq -r '.name' "$file") - maturity=$(jq -r '.maturity // "stable"' "$file") + $collectionFiles = Get-ChildItem -Path $collectionsDir -Filter '*.collection.yml' -File | Sort-Object Name + $matrixItems = @() - # Skip deprecated collections for all channels - if [ "$maturity" = "deprecated" ]; then - echo "::notice::Skipping deprecated collection: $id" >&2 - continue - fi + foreach ($file in $collectionFiles) { + $manifest = ConvertFrom-Yaml -Yaml (Get-Content -Path $file.FullName -Raw) + $id = [string]$manifest.id + $name = if ($manifest.ContainsKey('name')) { [string]$manifest.name } else { $id } + $maturity = if ($manifest.ContainsKey('maturity') -and $manifest.maturity) { [string]$manifest.maturity } else { 'stable' } - # Skip experimental collections for Stable channel - if [ "$channel" = "Stable" ] && [ "$maturity" = "experimental" ]; then - echo "::notice::Skipping experimental collection '$id' for Stable channel" >&2 + if ($maturity -eq 'deprecated') { + Write-Host "::notice::Skipping deprecated collection: $id" continue - fi + } - echo "{\"id\":\"$id\",\"name\":\"$name\",\"manifest\":\"$file\",\"maturity\":\"$maturity\"}" - done | jq -s '{include: .}') + if ($channel -eq 'Stable' -and $maturity -eq 'experimental') { + Write-Host "::notice::Skipping experimental collection '$id' for Stable channel" + continue + } + + $matrixItems += @{ + id = $id + name = $name + manifest = $file.FullName -replace '\\', '/' + maturity = $maturity + } + } - echo "matrix=$matrix_json" >> "$GITHUB_OUTPUT" - echo "Discovered collections:" - echo "$matrix_json" | jq . + $matrixJson = @{ include = $matrixItems } | ConvertTo-Json -Depth 5 -Compress + "matrix=$matrixJson" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + Write-Host "Discovered collections:" + $matrixJson | ConvertFrom-Json | ConvertTo-Json -Depth 5 package: name: Package ${{ matrix.id }} @@ -142,6 +144,11 @@ jobs: Write-Host "PowerShell version: $($PSVersionTable.PSVersion)" Install-Module -Name PowerShell-Yaml -Force -Scope CurrentUser + - name: Generate persona package templates + shell: pwsh + run: | + ./scripts/extension/Generate-ExtensionCollections.ps1 + - name: Download changelog artifact if: inputs.use-changelog uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 diff --git a/.gitignore b/.gitignore index 188a933d..d2540f3f 100644 --- a/.gitignore +++ b/.gitignore @@ -418,7 +418,10 @@ FodyWeavers.xsd # Extension build artifacts extension/LICENSE extension/CHANGELOG.md +extension/package.json +extension/package.*.json extension/package.json.bak +!extension/templates/package.template.json # Windows Installer files from build outputs *.cab diff --git a/docs/architecture/workflows.md b/docs/architecture/workflows.md index 0354bf23..cc366f0a 100644 --- a/docs/architecture/workflows.md +++ b/docs/architecture/workflows.md @@ -200,7 +200,7 @@ flowchart LR ### Collection-Based Packaging -Collection manifests in `extension/collections/*.collection.json` define persona-scoped subsets of the full artifact set. The `extension-package.yml` reusable workflow discovers these manifests, filters by maturity and channel, and packages each as an independent VSIX. +Collection manifests in `collections/*.collection.yml` define persona-scoped subsets of the full artifact set. The `extension-package.yml` reusable workflow discovers these manifests, filters by maturity and channel, and packages each as an independent VSIX. | Collection | Maturity | Included In | |----------------|--------------|--------------------| diff --git a/docs/contributing/release-process.md b/docs/contributing/release-process.md index a15a5b15..031f6a62 100644 --- a/docs/contributing/release-process.md +++ b/docs/contributing/release-process.md @@ -34,7 +34,7 @@ When you merge a PR to `main`: The Release PR is not a branch cut or deployment. It is a staging mechanism containing only version metadata changes: * Updated `package.json` version -* Updated `extension/package.json` version +* Updated `extension/templates/package.template.json` version * Updated `CHANGELOG.md` Your actual code changes are already on `main` from your feature PRs. The Release PR accumulates version and changelog updates until you are ready to release. diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index 7afd3bde..f90b8e01 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -15,7 +15,7 @@ extension/ โ”œโ”€โ”€ .github/ # Temporarily copied during packaging (removed after) โ”œโ”€โ”€ docs/templates/ # Temporarily copied during packaging (removed after) โ”œโ”€โ”€ scripts/dev-tools/ # Temporarily copied during packaging (removed after) -โ”œโ”€โ”€ package.json # Extension manifest with VS Code configuration +โ”œโ”€โ”€ package.json # Generated extension manifest (gitignored, created by Generate-ExtensionCollections.ps1)\nโ”œโ”€โ”€ templates/ # Source templates for package generation โ”œโ”€โ”€ .vscodeignore # Controls what gets packaged into the .vsix โ”œโ”€โ”€ README.md # Extension marketplace description โ”œโ”€โ”€ LICENSE # Copy of root LICENSE @@ -218,7 +218,7 @@ rm -rf .github scripts && cp -r ../.github . && mkdir -p scripts && cp -r ../scr ## Publishing the Extension -**Important:** Update version in `extension/package.json` before publishing. +**Important:** Versions are managed by `release-please` via `extension/templates/package.template.json`. Run `Generate-ExtensionCollections.ps1` before packaging to ensure generated files have the correct version. **Setup Personal Access Token (one-time):** @@ -270,11 +270,11 @@ code --install-extension hve-core-*.vsix ## Version Management -### Update Version in `package.json` +### How Versions Are Managed -1. Manually update version in `extension/package.json` -2. Run `scripts/extension/Prepare-Extension.ps1` to update agents/prompts/instructions -3. Run `scripts/extension/Package-Extension.ps1` to create the `.vsix` file +The version source of truth is `extension/templates/package.template.json`. The `release-please` automation updates this file's `version` field on releases. Running `Generate-ExtensionCollections.ps1` propagates the version to all generated `extension/package.json` and `extension/package.*.json` files. + +Generated package files are ephemeral build artifacts (gitignored). They are created by `Generate-ExtensionCollections.ps1` and consumed by `Prepare-Extension.ps1` and `Package-Extension.ps1` at build time. ### Development Builds @@ -355,28 +355,26 @@ The extension supports building persona-specific collection packages from a sing ### Available Collections -Collection manifests are defined in `extension/collections/`: - -| Collection | Manifest | Description | -|------------|--------------------------------|----------------------------------------| -| Full | `hve-core-all.collection.json` | All artifacts regardless of persona | -| Developer | `developer.collection.json` | Software engineering focused artifacts | +Collection manifests are defined in root `collections/` as YAML files: -### Persona Template Files +| Collection | Manifest | Description | +|------------|------------------------------------|----------------------------------------| +| Full | `hve-core-all.collection.yml` | All artifacts regardless of persona | +| Developer | `developer.collection.yml` | Software engineering focused artifacts | -Each persona collection has a corresponding `package.{collection-id}.json` template file in `extension/`. These files contain static metadata (name, display name, description, publisher) for the persona edition. The `contributes` section is empty because `Prepare-Extension.ps1` populates it dynamically at build time. +### Persona Package Files -| Template | Collection | Purpose | -|--------------------------|------------|-----------------------------------| -| `package.json` | Full | Canonical manifest (hve-core-all) | -| `package.developer.json` | Developer | Developer edition metadata | +All persona package files (`extension/package.json`, `extension/package.*.json`) are generated by `Generate-ExtensionCollections.ps1` from the source template and root collection YAML metadata. These files are gitignored build artifacts. -The canonical `extension/package.json` serves double duty: it is both the default build target and the `hve-core-all` template. No separate `package.hve-core-all.json` file exists. +| Generated File | Source Collection | Purpose | +|--------------------------|---------------------------------|-----------------------------------| +| `package.json` | `hve-core-all.collection.yml` | Full bundle manifest | +| `package.{id}.json` | `{id}.collection.yml` | Persona edition metadata | When building a persona collection, `Prepare-Extension.ps1`: 1. Backs up `package.json` to `package.json.bak` -2. Copies the persona template (`package.developer.json`) over `package.json` +2. Copies the persona template (`package.{id}.json`) over `package.json` 3. Generates `contributes` into the copied file 4. Serializes the result as `package.json` @@ -384,7 +382,7 @@ After packaging, `Package-Extension.ps1` restores the canonical `package.json` f #### Version Synchronization -Template files contain a `version` field managed by `release-please`. The `release-please-config.json` file includes `extra-files` entries for each template, ensuring versions stay synchronized across all persona templates and the canonical `package.json`. +`release-please` manages the version in `extension/templates/package.template.json`. The `Generate-ExtensionCollections.ps1` script propagates this version to all generated persona package files. No manual version updates are needed. ### Building Collection Packages @@ -396,31 +394,31 @@ pwsh ./scripts/extension/Prepare-Extension.ps1 pwsh ./scripts/extension/Package-Extension.ps1 # Build a persona-specific collection (copies persona template) -pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection extension/collections/developer.collection.json -pwsh ./scripts/extension/Package-Extension.ps1 -Collection extension/collections/developer.collection.json +pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection collections/developer.collection.yml +pwsh ./scripts/extension/Package-Extension.ps1 -Collection collections/developer.collection.yml ``` When `-Collection` targets a persona other than `hve-core-all`, the prepare script copies the persona template to `package.json` before generating `contributes`. The packaging script restores the canonical `package.json` after building. ### Inner Dev Loop -For rapid iteration without running the full build pipeline, copy the persona template manually: +For rapid iteration without running the full build pipeline: ```bash -# 1. Copy the developer template -cp extension/package.developer.json extension/package.json +# 1. Generate all package files from the template +pwsh ./scripts/extension/Generate-ExtensionCollections.ps1 -# 2. Run prepare to generate contributes -pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection extension/collections/developer.collection.json +# 2. Run prepare to filter artifacts for a collection +pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection collections/developer.collection.yml # 3. Inspect the result cat extension/package.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['name'], len(d.get('contributes',{}).get('chatAgents',[])),'agents')" -# 4. Restore canonical package.json -git checkout extension/package.json +# 4. Regenerate clean package files +pwsh ./scripts/extension/Generate-ExtensionCollections.ps1 ``` -The template file stays clean. Use `git checkout extension/package.json` to restore the canonical state at any time. +Generated package files are gitignored. Regenerate them at any time with `Generate-ExtensionCollections.ps1`. ### Collection Resolution @@ -459,7 +457,7 @@ To verify artifact inclusion before publishing: ```bash # 1. Prepare with collection filtering -pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection extension/collections/developer.collection.json -Verbose +pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection collections/developer.collection.yml -Verbose # 2. Check package.json for included artifacts cat extension/package.json | jq '.contributes.chatAgents' @@ -497,18 +495,18 @@ npm run lint:registry -- --verbose ### Collection Manifest Schema -Collection manifests follow this structure: - -```json -{ - "$schema": "../../scripts/linting/schemas/collection.schema.json", - "id": "developer", - "name": "hve-developer", - "displayName": "HVE Core - Developer Edition", - "description": "AI-powered coding agents curated for software engineers", - "maturity": "stable", - "personas": ["developer"] -} +Collection manifests are YAML files in `collections/` following this structure: + +```yaml +id: developer +name: hve-developer +displayName: "HVE Core - Developer Edition" +description: "AI-powered coding agents curated for software engineers" +maturity: stable +items: + - kind: agent + path: .github/agents/my-agent.agent.md + maturity: stable ``` | Field | Required | Description | @@ -539,23 +537,23 @@ Omitting the `maturity` field defaults to `stable`, maintaining backward compati To create a new persona collection: -1. Create a new manifest in `extension/collections/`: - - ```json - { - "$schema": "../../scripts/linting/schemas/collection.schema.json", - "id": "my-persona", - "name": "hve-my-persona", - "displayName": "HVE Core - My Persona Edition", - "description": "Description of artifacts included for this persona", - "maturity": "experimental", - "personas": ["my-persona"] - } +1. Create a new collection manifest in `collections/`: + + ```yaml + id: my-persona + name: hve-my-persona + displayName: "HVE Core - My Persona Edition" + description: "Description of artifacts included for this persona" + maturity: experimental + items: + - kind: agent + path: .github/agents/my-agent.agent.md + maturity: experimental ``` 2. Add the persona to the registry's `personas` section 3. Tag relevant artifacts with the new persona in the registry -4. Test the build locally with `-Collection my-persona` +4. Test the build locally with `-Collection collections/my-persona.collection.yml` 5. Submit PR with the new collection manifest > [!TIP] diff --git a/extension/collections/developer.collection.json b/extension/collections/developer.collection.json deleted file mode 100644 index d9c14cb8..00000000 --- a/extension/collections/developer.collection.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "../../scripts/linting/schemas/collection.schema.json", - "id": "developer", - "name": "hve-developer", - "displayName": "HVE Core - Developer Edition", - "description": "AI-powered coding agents and prompts curated for software engineers", - "maturity": "experimental", - "personas": [ - "developer" - ] -} \ No newline at end of file diff --git a/extension/collections/hve-core-all.collection.json b/extension/collections/hve-core-all.collection.json deleted file mode 100644 index d2b9f4f2..00000000 --- a/extension/collections/hve-core-all.collection.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "../../scripts/linting/schemas/collection.schema.json", - "id": "hve-core-all", - "name": "hve-core", - "displayName": "HVE Core", - "description": "AI-powered chat agents, prompts, and instructions for hybrid virtual environments", - "maturity": "stable", - "personas": [ - "hve-core-all" - ] -} \ No newline at end of file diff --git a/extension/hve-core-2.2.0.zip b/extension/hve-core-2.2.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..7069838e9225523c004c10686905b859f212b2cb GIT binary patch literal 300928 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

=h;Ejw6H*EnpEVvPLY?94 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 F{{edX^TPlD literal 0 HcmV?d00001 diff --git a/extension/package.developer.json b/extension/package.developer.json deleted file mode 100644 index 10748ef3..00000000 --- a/extension/package.developer.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "hve-developer", - "displayName": "HVE Core - Developer Edition", - "extensionKind": [ - "workspace", - "ui" - ], - "version": "2.1.0", - "description": "AI-powered coding agents and prompts curated for software engineers", - "publisher": "ise-hve-essentials", - "repository": { - "type": "git", - "url": "https://github.com/microsoft/hve-core.git" - }, - "engines": { - "vscode": "^1.106.1" - }, - "categories": [ - "Chat" - ], - "contributes": {}, - "author": "Microsoft", - "license": "MIT" -} diff --git a/extension/package.json b/extension/package.json deleted file mode 100644 index d184f3c5..00000000 --- a/extension/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "hve-core", - "displayName": "HVE Core", - "extensionKind": [ - "workspace", - "ui" - ], - "version": "2.2.0", - "description": "AI-powered chat agents, prompts, and instructions for hybrid virtual environments", - "publisher": "ise-hve-essentials", - "repository": { - "type": "git", - "url": "https://github.com/microsoft/hve-core.git" - }, - "engines": { - "vscode": "^1.106.1" - }, - "categories": [ - "Chat" - ], - "contributes": {}, - "author": "Microsoft", - "license": "MIT" -} diff --git a/extension/templates/collection.template.json b/extension/templates/collection.template.json deleted file mode 100644 index f4ff0981..00000000 --- a/extension/templates/collection.template.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "../../scripts/linting/schemas/collection.schema.json", - "id": "", - "name": "", - "displayName": "", - "description": "", - "maturity": "stable" -} diff --git a/release-please-config.json b/release-please-config.json index 6c11a09a..feed0c30 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -21,12 +21,7 @@ "extra-files": [ { "type": "json", - "path": "extension/package.json", - "jsonpath": "$.version" - }, - { - "type": "json", - "path": "extension/package.developer.json", + "path": "extension/templates/package.template.json", "jsonpath": "$.version" } ] diff --git a/scripts/extension/Generate-ExtensionCollections.ps1 b/scripts/extension/Generate-ExtensionCollections.ps1 index a3e0ff16..20422b0c 100644 --- a/scripts/extension/Generate-ExtensionCollections.ps1 +++ b/scripts/extension/Generate-ExtensionCollections.ps1 @@ -5,11 +5,10 @@ <# .SYNOPSIS - Generates extension collection manifests and package templates from root collections. + Generates persona package templates from root collections. .DESCRIPTION Reads root collection manifests from collections/*.collection.yml and generates: - - extension/collections/*.collection.json - extension/package.{collection-id}.json (plus canonical extension/package.json for hve-core-all) Generated output is deterministic for identical input files. @@ -28,21 +27,6 @@ $ErrorActionPreference = 'Stop' Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force -function Get-CollectionMaturity { - [CmdletBinding()] - [OutputType([string])] - param( - [Parameter(Mandatory = $true)] - [hashtable]$CollectionManifest - ) - - if ($CollectionManifest.ContainsKey('maturity') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionManifest.maturity)) { - return [string]$CollectionManifest.maturity - } - - return 'stable' -} - function Get-CollectionDisplayName { [CmdletBinding()] [OutputType([string])] @@ -130,16 +114,6 @@ function Remove-StaleGeneratedFiles { $null = $expected.Add([System.IO.Path]::GetFullPath($file)) } - $collectionsDir = Join-Path $RepoRoot 'extension/collections' - if (Test-Path $collectionsDir) { - Get-ChildItem -Path $collectionsDir -Filter '*.collection.json' -File | ForEach-Object { - $fullPath = [System.IO.Path]::GetFullPath($_.FullName) - if (-not $expected.Contains($fullPath)) { - Remove-Item -Path $_.FullName -Force - } - } - } - $extensionDir = Join-Path $RepoRoot 'extension' Get-ChildItem -Path $extensionDir -Filter 'package.*.json' -File | ForEach-Object { $fullPath = [System.IO.Path]::GetFullPath($_.FullName) @@ -158,7 +132,6 @@ function Get-GenerationHashes { ) $patterns = @( - (Join-Path $OutputRoot 'extension/collections/*.collection.json'), (Join-Path $OutputRoot 'extension/package.json'), (Join-Path $OutputRoot 'extension/package.*.json') ) @@ -192,15 +165,10 @@ function Invoke-ExtensionCollectionsGeneration { ) $collectionsDir = Join-Path $RepoRoot 'collections' - $extensionCollectionsDir = Join-Path $RepoRoot 'extension/collections' $templatesDir = Join-Path $RepoRoot 'extension/templates' - $collectionTemplatePath = Join-Path $templatesDir 'collection.template.json' $packageTemplatePath = Join-Path $templatesDir 'package.template.json' - if (-not (Test-Path $collectionTemplatePath)) { - throw "Collection template not found: $collectionTemplatePath" - } if (-not (Test-Path $packageTemplatePath)) { throw "Package template not found: $packageTemplatePath" } @@ -211,7 +179,6 @@ function Invoke-ExtensionCollectionsGeneration { Import-Module PowerShell-Yaml -ErrorAction Stop - $collectionTemplate = Get-Content -Path $collectionTemplatePath -Raw | ConvertFrom-Json $packageTemplate = Get-Content -Path $packageTemplatePath -Raw | ConvertFrom-Json $collectionFiles = Get-ChildItem -Path $collectionsDir -Filter '*.collection.yml' -File | Sort-Object Name @@ -219,8 +186,6 @@ function Invoke-ExtensionCollectionsGeneration { throw "No root collection files found in $collectionsDir" } - New-Item -Path $extensionCollectionsDir -ItemType Directory -Force | Out-Null - $expectedFiles = @() foreach ($collectionFile in $collectionFiles) { @@ -235,7 +200,6 @@ function Invoke-ExtensionCollectionsGeneration { } $collectionDescription = if ($collection.ContainsKey('description')) { [string]$collection.description } else { [string]$packageTemplate.description } - $collectionMaturity = Get-CollectionMaturity -CollectionManifest $collection $extensionName = if ($collectionId -eq 'hve-core-all') { [string]$packageTemplate.name } else { "hve-$collectionId" } $extensionDisplayName = if ($collectionId -eq 'hve-core-all') { @@ -245,18 +209,6 @@ function Invoke-ExtensionCollectionsGeneration { Get-CollectionDisplayName -CollectionManifest $collection -DefaultValue ([string]$packageTemplate.displayName) } - $collectionManifest = Copy-TemplateWithOverrides -Template $collectionTemplate -Overrides @{ - id = $collectionId - name = $extensionName - displayName = $extensionDisplayName - description = $collectionDescription - maturity = $collectionMaturity - } - - $collectionManifestPath = Join-Path $extensionCollectionsDir "$collectionId.collection.json" - Set-JsonFile -Path $collectionManifestPath -Content $collectionManifest - $expectedFiles += $collectionManifestPath - $packageTemplateOutput = Copy-TemplateWithOverrides -Template $packageTemplate -Overrides @{ name = $extensionName displayName = $extensionDisplayName diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index 1944b376..b8099bce 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -28,7 +28,7 @@ Uses vsce --pre-release flag which marks the extension for the pre-release track. .PARAMETER Collection - Optional. Path to a collection manifest JSON file. When specified, only + Optional. Path to a collection manifest file (YAML or JSON). When specified, only collection-filtered artifacts are copied and the output filename uses the collection ID. @@ -260,9 +260,10 @@ function Get-PersonaReadmePath { .DESCRIPTION Maps a collection manifest to its persona-specific README file. Returns null when the collection is the full package (hve-core-all) or when no - matching persona README exists on disk. + matching persona README exists on disk. Supports both YAML and JSON + manifest formats. .PARAMETER CollectionPath - Path to the collection manifest JSON file. + Path to the collection manifest file (YAML or JSON). .PARAMETER ExtensionDirectory Path to the extension directory containing README files. .OUTPUTS @@ -278,7 +279,13 @@ function Get-PersonaReadmePath { [string]$ExtensionDirectory ) - $manifest = Get-Content -Path $CollectionPath -Raw | ConvertFrom-Json + $extension = [System.IO.Path]::GetExtension($CollectionPath).ToLowerInvariant() + if ($extension -in @('.yml', '.yaml')) { + $manifest = ConvertFrom-Yaml -Yaml (Get-Content -Path $CollectionPath -Raw) + } + else { + $manifest = Get-Content -Path $CollectionPath -Raw | ConvertFrom-Json + } $collectionId = $manifest.id # Full package uses the default README.md @@ -763,7 +770,7 @@ function Invoke-PackageExtension { .PARAMETER PreRelease Switch to mark the package as a pre-release version. .PARAMETER Collection - Optional path to a collection manifest JSON file. When specified, only + Optional path to a collection manifest file (YAML or JSON). When specified, only collection-filtered artifacts are copied and the output filename uses the collection ID. .PARAMETER DryRun diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index 88ea9917..f139336f 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -156,12 +156,13 @@ function Test-CollectionMaturityEligible { function Get-CollectionManifest { <# .SYNOPSIS - Loads a collection manifest JSON file. + Loads a collection manifest from a YAML or JSON file. .DESCRIPTION - Reads and parses a collection manifest JSON file that defines persona-based - artifact filtering rules for extension packaging. + Reads and parses a collection manifest file that defines persona-based + artifact filtering rules for extension packaging. Supports both YAML + (.yml/.yaml) and JSON (.json) formats. .PARAMETER CollectionPath - Path to the collection manifest JSON file. + Path to the collection manifest file (YAML or JSON). .OUTPUTS [hashtable] Parsed collection manifest with id, name, displayName, description, personas, and optional include/exclude. #> @@ -177,6 +178,12 @@ function Get-CollectionManifest { throw "Collection manifest not found: $CollectionPath" } + $extension = [System.IO.Path]::GetExtension($CollectionPath).ToLowerInvariant() + if ($extension -in @('.yml', '.yaml')) { + $content = Get-Content -Path $CollectionPath -Raw + return ConvertFrom-Yaml -Yaml $content + } + $content = Get-Content -Path $CollectionPath -Raw return $content | ConvertFrom-Json -AsHashtable } @@ -1157,13 +1164,15 @@ function Invoke-PrepareExtension { $artifactCollectionManifest = $collectionManifest if (-not $artifactCollectionManifest.ContainsKey('items') -or @($artifactCollectionManifest.items).Count -eq 0) { + # When the manifest lacks items (e.g., a generated JSON template), + # resolve from the root YAML collection by ID. $rootCollectionPath = Join-Path $RepoRoot "collections/$($collectionManifest.id).collection.yml" if (Test-Path $rootCollectionPath) { $artifactCollectionManifest = ConvertFrom-Yaml -Yaml (Get-Content -Path $rootCollectionPath -Raw) - Write-Host "Using root collection metadata: $rootCollectionPath" + Write-Host "Using root collection for items: $rootCollectionPath" } else { - Write-Warning "No root collection metadata found for '$($collectionManifest.id)' at $rootCollectionPath" + Write-Warning "No root collection found for '$($collectionManifest.id)' at $rootCollectionPath" } } diff --git a/scripts/tests/extension/Package-Extension.Tests.ps1 b/scripts/tests/extension/Package-Extension.Tests.ps1 index c5f4b8f3..d7e9b09d 100644 --- a/scripts/tests/extension/Package-Extension.Tests.ps1 +++ b/scripts/tests/extension/Package-Extension.Tests.ps1 @@ -938,16 +938,22 @@ Describe 'Get-PersonaReadmePath' { } It 'Returns null for hve-core-all collection' { - $collectionPath = Join-Path $script:testDir 'collection.json' - @{ id = 'hve-core-all'; name = 'all' } | ConvertTo-Json | Set-Content $collectionPath + $collectionPath = Join-Path $script:testDir 'collection.yml' + @" +id: hve-core-all +name: all +"@ | Set-Content $collectionPath $result = Get-PersonaReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir $result | Should -BeNullOrEmpty } It 'Returns persona README path when file exists' { - $collectionPath = Join-Path $script:testDir 'collection.json' - @{ id = 'developer'; name = 'dev' } | ConvertTo-Json | Set-Content $collectionPath + $collectionPath = Join-Path $script:testDir 'collection.yml' + @" +id: developer +name: dev +"@ | Set-Content $collectionPath $personaReadme = Join-Path $script:extDir 'README.developer.md' Set-Content -Path $personaReadme -Value '# Developer README' @@ -957,8 +963,11 @@ Describe 'Get-PersonaReadmePath' { } It 'Returns null when persona README file does not exist' { - $collectionPath = Join-Path $script:testDir 'collection.json' - @{ id = 'security'; name = 'sec' } | ConvertTo-Json | Set-Content $collectionPath + $collectionPath = Join-Path $script:testDir 'collection.yml' + @" +id: security +name: sec +"@ | Set-Content $collectionPath $result = Get-PersonaReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir $result | Should -BeNullOrEmpty @@ -1182,8 +1191,14 @@ Describe 'Invoke-PackageExtension - Collection mode' { Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } } Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } } - $collectionPath = Join-Path $script:testRoot 'collection.json' - @{ id = 'developer'; name = 'dev'; displayName = 'Developer'; personas = @('developer') } | ConvertTo-Json | Set-Content $collectionPath + $collectionPath = Join-Path $script:testRoot 'collection.yml' + @" +id: developer +name: dev +displayName: Developer +personas: + - developer +"@ | Set-Content $collectionPath $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix' Set-Content -Path $vsixPath -Value 'fake-vsix' @@ -1196,8 +1211,14 @@ Describe 'Invoke-PackageExtension - Collection mode' { Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } } Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } } - $collectionPath = Join-Path $script:testRoot 'collection.json' - @{ id = 'developer'; name = 'dev'; displayName = 'Developer'; personas = @('developer') } | ConvertTo-Json | Set-Content $collectionPath + $collectionPath = Join-Path $script:testRoot 'collection.yml' + @" +id: developer +name: dev +displayName: Developer +personas: + - developer +"@ | Set-Content $collectionPath # Create persona README in extension directory Set-Content -Path (Join-Path $script:extDir 'README.developer.md') -Value '# Developer Persona' diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index 652d615d..217f54e9 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -359,10 +359,26 @@ Describe 'Get-CollectionManifest' { Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue } - It 'Loads collection manifest from valid path' { + It 'Loads collection manifest from valid YAML path' { + $manifestFile = Join-Path $script:tempDir 'test.collection.yml' + @" +id: test +name: test-ext +displayName: Test Extension +description: Test +personas: + - hve-core-all +"@ | Set-Content -Path $manifestFile + + $result = Get-CollectionManifest -CollectionPath $manifestFile + $result | Should -Not -BeNullOrEmpty + $result.id | Should -Be 'test' + } + + It 'Loads collection manifest from valid JSON path' { $manifestFile = Join-Path $script:tempDir 'test.collection.json' @{ - '$schema' = '../schemas/collection.schema.json' + '\$schema' = '../schemas/collection.schema.json' id = 'test' name = 'test-ext' displayName = 'Test Extension' @@ -381,15 +397,15 @@ Describe 'Get-CollectionManifest' { } It 'Returns hashtable with expected keys' { - $manifestFile = Join-Path $script:tempDir 'keys.collection.json' - @{ - '$schema' = '../schemas/collection.schema.json' - id = 'keys' - name = 'keys-ext' - displayName = 'Keys' - description = 'Keys test' - personas = @('developer') - } | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestFile + $manifestFile = Join-Path $script:tempDir 'keys.collection.yml' + @" +id: keys +name: keys-ext +displayName: Keys +description: Keys test +personas: + - developer +"@ | Set-Content -Path $manifestFile $result = Get-CollectionManifest -CollectionPath $manifestFile $result.Keys | Should -Contain 'id' @@ -748,18 +764,22 @@ description: "Preview agent" --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'preview.agent.md') - $collectionPath = Join-Path $script:tempDir 'channel-filter.collection.json' - @{ - id = 'hve-core-all' - name = 'hve-core-all' - displayName = 'HVE Core - All' - description = 'Channel filtering test' - personas = @('hve-core-all') - items = @( - @{ kind = 'agent'; path = '.github/agents/test.agent.md'; maturity = 'stable' }, - @{ kind = 'agent'; path = '.github/agents/preview.agent.md'; maturity = 'preview' } - ) - } | ConvertTo-Json -Depth 8 | Set-Content -Path $collectionPath + $collectionPath = Join-Path $script:tempDir 'channel-filter.collection.yml' + @" +id: hve-core-all +name: hve-core-all +displayName: HVE Core - All +description: Channel filtering test +personas: + - hve-core-all +items: + - kind: agent + path: .github/agents/test.agent.md + maturity: stable + - kind: agent + path: .github/agents/preview.agent.md + maturity: preview +"@ | Set-Content -Path $collectionPath $stableResult = Invoke-PrepareExtension ` -ExtensionDirectory $script:extDir ` @@ -794,21 +814,31 @@ applyTo: "**/*.js" --- '@ | Set-Content -Path (Join-Path $script:instrDir 'preview.instructions.md') - $collectionPath = Join-Path $script:tempDir 'prompt-instruction-filter.collection.json' - @{ - id = 'hve-core-all' - name = 'hve-core-all' - displayName = 'HVE Core - All' - description = 'Prompt/instruction filtering test' - personas = @('hve-core-all') - items = @( - @{ kind = 'agent'; path = '.github/agents/test.agent.md'; maturity = 'stable' }, - @{ kind = 'prompt'; path = '.github/prompts/test.prompt.md'; maturity = 'stable' }, - @{ kind = 'prompt'; path = '.github/prompts/experimental.prompt.md'; maturity = 'experimental' }, - @{ kind = 'instruction'; path = '.github/instructions/test.instructions.md'; maturity = 'stable' }, - @{ kind = 'instruction'; path = '.github/instructions/preview.instructions.md'; maturity = 'preview' } - ) - } | ConvertTo-Json -Depth 8 | Set-Content -Path $collectionPath + $collectionPath = Join-Path $script:tempDir 'prompt-instruction-filter.collection.yml' + @" +id: hve-core-all +name: hve-core-all +displayName: HVE Core - All +description: Prompt/instruction filtering test +personas: + - hve-core-all +items: + - kind: agent + path: .github/agents/test.agent.md + maturity: stable + - kind: prompt + path: .github/prompts/test.prompt.md + maturity: stable + - kind: prompt + path: .github/prompts/experimental.prompt.md + maturity: experimental + - kind: instruction + path: .github/instructions/test.instructions.md + maturity: stable + - kind: instruction + path: .github/instructions/preview.instructions.md + maturity: preview +"@ | Set-Content -Path $collectionPath $stableResult = Invoke-PrepareExtension ` -ExtensionDirectory $script:extDir ` @@ -913,34 +943,37 @@ applyTo: "**/*.js" Context 'Persona template copy' { BeforeAll { # Developer collection manifest - $script:devCollectionPath = Join-Path $script:tempDir 'developer.collection.json' - @{ - id = 'developer' - name = 'hve-developer' - displayName = 'HVE Core - Developer Edition' - description = 'Developer edition' - personas = @('developer') - } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:devCollectionPath + $script:devCollectionPath = Join-Path $script:tempDir 'developer.collection.yml' + @" +id: developer +name: hve-developer +displayName: HVE Core - Developer Edition +description: Developer edition +personas: + - developer +"@ | Set-Content -Path $script:devCollectionPath # hve-core-all collection manifest (default) - $script:allCollectionPath = Join-Path $script:tempDir 'hve-core-all.collection.json' - @{ - id = 'hve-core-all' - name = 'hve-core-all' - displayName = 'HVE Core - All' - description = 'All artifacts' - personas = @('hve-core-all') - } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:allCollectionPath + $script:allCollectionPath = Join-Path $script:tempDir 'hve-core-all.collection.yml' + @" +id: hve-core-all +name: hve-core-all +displayName: HVE Core - All +description: All artifacts +personas: + - hve-core-all +"@ | Set-Content -Path $script:allCollectionPath # Collection manifest referencing a missing template - $script:missingCollectionPath = Join-Path $script:tempDir 'nonexistent.collection.json' - @{ - id = 'nonexistent' - name = 'nonexistent' - displayName = 'Nonexistent' - description = 'Missing template' - personas = @('nonexistent') - } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:missingCollectionPath + $script:missingCollectionPath = Join-Path $script:tempDir 'nonexistent.collection.yml' + @" +id: nonexistent +name: nonexistent +displayName: Nonexistent +description: Missing template +personas: + - nonexistent +"@ | Set-Content -Path $script:missingCollectionPath # Persona template for developer collection @' @@ -1034,26 +1067,28 @@ applyTo: "**/*.js" Context 'Collection maturity gating' { BeforeAll { # Deprecated collection manifest - $script:deprecatedCollectionPath = Join-Path $script:tempDir 'deprecated.collection.json' - @{ - id = 'deprecated-coll' - name = 'deprecated-ext' - displayName = 'Deprecated Collection' - description = 'Deprecated collection for testing' - personas = @('hve-core-all') - maturity = 'deprecated' - } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:deprecatedCollectionPath + $script:deprecatedCollectionPath = Join-Path $script:tempDir 'deprecated.collection.yml' + @" +id: deprecated-coll +name: deprecated-ext +displayName: Deprecated Collection +description: Deprecated collection for testing +personas: + - hve-core-all +maturity: deprecated +"@ | Set-Content -Path $script:deprecatedCollectionPath # Experimental collection manifest - $script:experimentalCollectionPath = Join-Path $script:tempDir 'experimental.collection.json' - @{ - id = 'experimental-coll' - name = 'experimental-ext' - displayName = 'Experimental Collection' - description = 'Experimental collection for testing' - personas = @('hve-core-all') - maturity = 'experimental' - } | ConvertTo-Json -Depth 5 | Set-Content -Path $script:experimentalCollectionPath + $script:experimentalCollectionPath = Join-Path $script:tempDir 'experimental.collection.yml' + @" +id: experimental-coll +name: experimental-ext +displayName: Experimental Collection +description: Experimental collection for testing +personas: + - hve-core-all +maturity: experimental +"@ | Set-Content -Path $script:experimentalCollectionPath # Persona template for experimental collection @' From 2dd3b8257f518c9573ea0b9f5f884ac17551e83b Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 22:02:38 -0800 Subject: [PATCH 49/62] feat(extension): implement persona package generation from collection manifests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add functions for generating package files from collection manifests - remove old Generate-ExtensionCollections.ps1 script - integrate package generation into Prepare-Extension.ps1 - add tests for new package generation functions ๐Ÿ”ง - Generated by Copilot --- .github/workflows/extension-package.yml | 5 - extension/PACKAGING.md | 25 +- .../Generate-ExtensionCollections.ps1 | 283 -------------- scripts/extension/Prepare-Extension.ps1 | 250 +++++++++++- .../extension/Prepare-Extension.Tests.ps1 | 364 ++++++++++++++---- 5 files changed, 549 insertions(+), 378 deletions(-) delete mode 100644 scripts/extension/Generate-ExtensionCollections.ps1 diff --git a/.github/workflows/extension-package.yml b/.github/workflows/extension-package.yml index 8f8beda1..3bc4e7f1 100644 --- a/.github/workflows/extension-package.yml +++ b/.github/workflows/extension-package.yml @@ -144,11 +144,6 @@ jobs: Write-Host "PowerShell version: $($PSVersionTable.PSVersion)" Install-Module -Name PowerShell-Yaml -Force -Scope CurrentUser - - name: Generate persona package templates - shell: pwsh - run: | - ./scripts/extension/Generate-ExtensionCollections.ps1 - - name: Download changelog artifact if: inputs.use-changelog uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index f90b8e01..b938a0fd 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -15,7 +15,7 @@ extension/ โ”œโ”€โ”€ .github/ # Temporarily copied during packaging (removed after) โ”œโ”€โ”€ docs/templates/ # Temporarily copied during packaging (removed after) โ”œโ”€โ”€ scripts/dev-tools/ # Temporarily copied during packaging (removed after) -โ”œโ”€โ”€ package.json # Generated extension manifest (gitignored, created by Generate-ExtensionCollections.ps1)\nโ”œโ”€โ”€ templates/ # Source templates for package generation +โ”œโ”€โ”€ package.json # Generated extension manifest (gitignored, created by Prepare-Extension.ps1)\nโ”œโ”€โ”€ templates/ # Source templates for package generation โ”œโ”€โ”€ .vscodeignore # Controls what gets packaged into the .vsix โ”œโ”€โ”€ README.md # Extension marketplace description โ”œโ”€โ”€ LICENSE # Copy of root LICENSE @@ -218,7 +218,7 @@ rm -rf .github scripts && cp -r ../.github . && mkdir -p scripts && cp -r ../scr ## Publishing the Extension -**Important:** Versions are managed by `release-please` via `extension/templates/package.template.json`. Run `Generate-ExtensionCollections.ps1` before packaging to ensure generated files have the correct version. +**Important:** Versions are managed by `release-please` via `extension/templates/package.template.json`. The `Prepare-Extension.ps1` script generates all persona package files with the correct version before preparing the extension. **Setup Personal Access Token (one-time):** @@ -272,9 +272,9 @@ code --install-extension hve-core-*.vsix ### How Versions Are Managed -The version source of truth is `extension/templates/package.template.json`. The `release-please` automation updates this file's `version` field on releases. Running `Generate-ExtensionCollections.ps1` propagates the version to all generated `extension/package.json` and `extension/package.*.json` files. +The version source of truth is `extension/templates/package.template.json`. The `release-please` automation updates this file's `version` field on releases. `Prepare-Extension.ps1` generates all `extension/package.json` and `extension/package.*.json` files from the template before performing artifact discovery. -Generated package files are ephemeral build artifacts (gitignored). They are created by `Generate-ExtensionCollections.ps1` and consumed by `Prepare-Extension.ps1` and `Package-Extension.ps1` at build time. +Generated package files are ephemeral build artifacts (gitignored). They are created and consumed by `Prepare-Extension.ps1` and `Package-Extension.ps1` at build time. ### Development Builds @@ -364,7 +364,7 @@ Collection manifests are defined in root `collections/` as YAML files: ### Persona Package Files -All persona package files (`extension/package.json`, `extension/package.*.json`) are generated by `Generate-ExtensionCollections.ps1` from the source template and root collection YAML metadata. These files are gitignored build artifacts. +All persona package files (`extension/package.json`, `extension/package.*.json`) are generated by `Prepare-Extension.ps1` from the source template and root collection YAML metadata. These files are gitignored build artifacts. | Generated File | Source Collection | Purpose | |--------------------------|---------------------------------|-----------------------------------| @@ -382,7 +382,7 @@ After packaging, `Package-Extension.ps1` restores the canonical `package.json` f #### Version Synchronization -`release-please` manages the version in `extension/templates/package.template.json`. The `Generate-ExtensionCollections.ps1` script propagates this version to all generated persona package files. No manual version updates are needed. +`release-please` manages the version in `extension/templates/package.template.json`. The `Prepare-Extension.ps1` script generates all persona package files with the propagated version. No manual version updates are needed. ### Building Collection Packages @@ -405,20 +405,17 @@ When `-Collection` targets a persona other than `hve-core-all`, the prepare scri For rapid iteration without running the full build pipeline: ```bash -# 1. Generate all package files from the template -pwsh ./scripts/extension/Generate-ExtensionCollections.ps1 - -# 2. Run prepare to filter artifacts for a collection +# 1. Prepare the extension (generates package files and discovers artifacts) pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection collections/developer.collection.yml -# 3. Inspect the result +# 2. Inspect the result cat extension/package.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['name'], len(d.get('contributes',{}).get('chatAgents',[])),'agents')" -# 4. Regenerate clean package files -pwsh ./scripts/extension/Generate-ExtensionCollections.ps1 +# 3. Regenerate clean package files with a fresh prepare +pwsh ./scripts/extension/Prepare-Extension.ps1 ``` -Generated package files are gitignored. Regenerate them at any time with `Generate-ExtensionCollections.ps1`. +Generated package files are gitignored. Each `Prepare-Extension.ps1` invocation regenerates them from the template. ### Collection Resolution diff --git a/scripts/extension/Generate-ExtensionCollections.ps1 b/scripts/extension/Generate-ExtensionCollections.ps1 deleted file mode 100644 index 20422b0c..00000000 --- a/scripts/extension/Generate-ExtensionCollections.ps1 +++ /dev/null @@ -1,283 +0,0 @@ -#!/usr/bin/env pwsh -# Copyright (c) Microsoft Corporation. -# SPDX-License-Identifier: MIT -#Requires -Version 7.0 - -<# -.SYNOPSIS - Generates persona package templates from root collections. - -.DESCRIPTION - Reads root collection manifests from collections/*.collection.yml and generates: - - extension/package.{collection-id}.json (plus canonical extension/package.json for hve-core-all) - - Generated output is deterministic for identical input files. - -.PARAMETER ValidateDeterminism - Runs generation twice in temporary directories and fails when outputs differ. -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory = $false)] - [switch]$ValidateDeterminism -) - -$ErrorActionPreference = 'Stop' - -Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force - -function Get-CollectionDisplayName { - [CmdletBinding()] - [OutputType([string])] - param( - [Parameter(Mandatory = $true)] - [hashtable]$CollectionManifest, - - [Parameter(Mandatory = $true)] - [string]$DefaultValue - ) - - if ($CollectionManifest.ContainsKey('displayName') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionManifest.displayName)) { - return [string]$CollectionManifest.displayName - } - - if ($CollectionManifest.ContainsKey('name') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionManifest.name)) { - return "HVE Core - $($CollectionManifest.name)" - } - - return $DefaultValue -} - -function Copy-TemplateWithOverrides { - [CmdletBinding()] - [OutputType([pscustomobject])] - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Template, - - [Parameter(Mandatory = $true)] - [hashtable]$Overrides - ) - - $output = [ordered]@{} - - foreach ($propertyName in $Template.PSObject.Properties.Name) { - if ($Overrides.ContainsKey($propertyName)) { - $output[$propertyName] = $Overrides[$propertyName] - } - else { - $output[$propertyName] = $Template.$propertyName - } - } - - foreach ($propertyName in $Overrides.Keys | Sort-Object) { - if (-not $output.Contains($propertyName)) { - $output[$propertyName] = $Overrides[$propertyName] - } - } - - return [pscustomobject]$output -} - -function Set-JsonFile { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Path, - - [Parameter(Mandatory = $true)] - [object]$Content - ) - - $parent = Split-Path -Path $Path -Parent - if (-not (Test-Path -Path $parent)) { - New-Item -Path $parent -ItemType Directory -Force | Out-Null - } - - $json = $Content | ConvertTo-Json -Depth 30 - Set-Content -Path $Path -Value $json -Encoding utf8NoBOM -} - -function Remove-StaleGeneratedFiles { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$RepoRoot, - - [Parameter(Mandatory = $true)] - [string[]]$ExpectedFiles - ) - - $expected = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) - foreach ($file in $ExpectedFiles) { - $null = $expected.Add([System.IO.Path]::GetFullPath($file)) - } - - $extensionDir = Join-Path $RepoRoot 'extension' - Get-ChildItem -Path $extensionDir -Filter 'package.*.json' -File | ForEach-Object { - $fullPath = [System.IO.Path]::GetFullPath($_.FullName) - if (-not $expected.Contains($fullPath)) { - Remove-Item -Path $_.FullName -Force - } - } -} - -function Get-GenerationHashes { - [CmdletBinding()] - [OutputType([string[]])] - param( - [Parameter(Mandatory = $true)] - [string]$OutputRoot - ) - - $patterns = @( - (Join-Path $OutputRoot 'extension/package.json'), - (Join-Path $OutputRoot 'extension/package.*.json') - ) - - $files = @() - foreach ($pattern in $patterns) { - $files += @(Get-ChildItem -Path $pattern -File -ErrorAction SilentlyContinue) - } - - $normalizedOutputRoot = [System.IO.Path]::GetFullPath($OutputRoot) - - $hashes = @( - $files | - Sort-Object FullName | - ForEach-Object { - $relativePath = [System.IO.Path]::GetRelativePath($normalizedOutputRoot, $_.FullName) -replace '\\', '/' - $hash = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash - "$relativePath::$hash" - } - ) - - return $hashes -} - -function Invoke-ExtensionCollectionsGeneration { - [CmdletBinding()] - [OutputType([string[]])] - param( - [Parameter(Mandatory = $true)] - [string]$RepoRoot - ) - - $collectionsDir = Join-Path $RepoRoot 'collections' - $templatesDir = Join-Path $RepoRoot 'extension/templates' - - $packageTemplatePath = Join-Path $templatesDir 'package.template.json' - - if (-not (Test-Path $packageTemplatePath)) { - throw "Package template not found: $packageTemplatePath" - } - - if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) { - throw "Required module 'PowerShell-Yaml' is not installed." - } - - Import-Module PowerShell-Yaml -ErrorAction Stop - - $packageTemplate = Get-Content -Path $packageTemplatePath -Raw | ConvertFrom-Json - - $collectionFiles = Get-ChildItem -Path $collectionsDir -Filter '*.collection.yml' -File | Sort-Object Name - if ($collectionFiles.Count -eq 0) { - throw "No root collection files found in $collectionsDir" - } - - $expectedFiles = @() - - foreach ($collectionFile in $collectionFiles) { - $collection = ConvertFrom-Yaml -Yaml (Get-Content -Path $collectionFile.FullName -Raw) - if ($collection -isnot [hashtable]) { - throw "Collection manifest must be a hashtable: $($collectionFile.FullName)" - } - - $collectionId = [string]$collection.id - if ([string]::IsNullOrWhiteSpace($collectionId)) { - throw "Collection id is required: $($collectionFile.FullName)" - } - - $collectionDescription = if ($collection.ContainsKey('description')) { [string]$collection.description } else { [string]$packageTemplate.description } - - $extensionName = if ($collectionId -eq 'hve-core-all') { [string]$packageTemplate.name } else { "hve-$collectionId" } - $extensionDisplayName = if ($collectionId -eq 'hve-core-all') { - [string]$packageTemplate.displayName - } - else { - Get-CollectionDisplayName -CollectionManifest $collection -DefaultValue ([string]$packageTemplate.displayName) - } - - $packageTemplateOutput = Copy-TemplateWithOverrides -Template $packageTemplate -Overrides @{ - name = $extensionName - displayName = $extensionDisplayName - description = $collectionDescription - } - - $packagePath = if ($collectionId -eq 'hve-core-all') { - Join-Path $RepoRoot 'extension/package.json' - } - else { - Join-Path $RepoRoot "extension/package.$collectionId.json" - } - - Set-JsonFile -Path $packagePath -Content $packageTemplateOutput - $expectedFiles += $packagePath - } - - Remove-StaleGeneratedFiles -RepoRoot $RepoRoot -ExpectedFiles $expectedFiles - - return $expectedFiles -} - -if ($MyInvocation.InvocationName -ne '.') { - try { - $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path - $repoRoot = (Get-Item "$scriptDir/../..").FullName - - $generated = Invoke-ExtensionCollectionsGeneration -RepoRoot $repoRoot - - if ($ValidateDeterminism) { - $tempA = Join-Path ([System.IO.Path]::GetTempPath()) ("hve-extgen-A-" + [System.Guid]::NewGuid().ToString('N')) - $tempB = Join-Path ([System.IO.Path]::GetTempPath()) ("hve-extgen-B-" + [System.Guid]::NewGuid().ToString('N')) - - try { - New-Item -Path $tempA -ItemType Directory -Force | Out-Null - New-Item -Path $tempB -ItemType Directory -Force | Out-Null - New-Item -Path (Join-Path $tempA 'extension') -ItemType Directory -Force | Out-Null - New-Item -Path (Join-Path $tempB 'extension') -ItemType Directory -Force | Out-Null - - Copy-Item -Path (Join-Path $repoRoot 'collections') -Destination $tempA -Recurse - Copy-Item -Path (Join-Path $repoRoot 'collections') -Destination $tempB -Recurse - Copy-Item -Path (Join-Path $repoRoot 'extension/templates') -Destination (Join-Path $tempA 'extension/templates') -Recurse - Copy-Item -Path (Join-Path $repoRoot 'extension/templates') -Destination (Join-Path $tempB 'extension/templates') -Recurse - - Invoke-ExtensionCollectionsGeneration -RepoRoot $tempA | Out-Null - Invoke-ExtensionCollectionsGeneration -RepoRoot $tempB | Out-Null - - $hashesA = Get-GenerationHashes -OutputRoot $tempA - $hashesB = Get-GenerationHashes -OutputRoot $tempB - - $contentA = ($hashesA -join "`n") - $contentB = ($hashesB -join "`n") - if ($contentA -ne $contentB) { - throw 'Determinism validation failed: generated outputs differ for identical inputs.' - } - } - finally { - if (Test-Path $tempA) { Remove-Item -Path $tempA -Recurse -Force } - if (Test-Path $tempB) { Remove-Item -Path $tempB -Recurse -Force } - } - } - - Write-Host "Generated $($generated.Count) extension artifacts." -ForegroundColor Green - Set-CIOutput -Name 'generated-files-count' -Value $generated.Count - exit 0 - } - catch { - Write-Error -ErrorAction Continue "Generate-ExtensionCollections failed: $($_.Exception.Message)" - Write-CIAnnotation -Message $_.Exception.Message -Level Error - exit 1 - } -} diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index f139336f..f835c913 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -65,6 +65,243 @@ Import-Module (Join-Path $PSScriptRoot "../lib/Modules/CIHelpers.psm1") -Force #region Pure Functions +#region Package Generation Functions + +function Get-CollectionDisplayName { + <# + .SYNOPSIS + Resolves a display name from a collection manifest. + .DESCRIPTION + Returns the displayName field if set, derives one from the name field, + or falls back to a default value. + .PARAMETER CollectionManifest + Parsed collection manifest hashtable. + .PARAMETER DefaultValue + Fallback display name when the manifest provides neither displayName nor name. + .OUTPUTS + [string] Resolved display name. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$CollectionManifest, + + [Parameter(Mandatory = $true)] + [string]$DefaultValue + ) + + if ($CollectionManifest.ContainsKey('displayName') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionManifest.displayName)) { + return [string]$CollectionManifest.displayName + } + + if ($CollectionManifest.ContainsKey('name') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionManifest.name)) { + return "HVE Core - $($CollectionManifest.name)" + } + + return $DefaultValue +} + +function Copy-TemplateWithOverrides { + <# + .SYNOPSIS + Clones a template object and applies field overrides. + .DESCRIPTION + Copies all properties from Template, replacing any whose key appears in + Overrides. Additional override keys not in the template are appended. + .PARAMETER Template + Source PSCustomObject to clone. + .PARAMETER Overrides + Hashtable of field values to override or add. + .OUTPUTS + [pscustomobject] New object with overrides applied. + #> + [CmdletBinding()] + [OutputType([pscustomobject])] + param( + [Parameter(Mandatory = $true)] + [pscustomobject]$Template, + + [Parameter(Mandatory = $true)] + [hashtable]$Overrides + ) + + $output = [ordered]@{} + + foreach ($propertyName in $Template.PSObject.Properties.Name) { + if ($Overrides.ContainsKey($propertyName)) { + $output[$propertyName] = $Overrides[$propertyName] + } + else { + $output[$propertyName] = $Template.$propertyName + } + } + + foreach ($propertyName in $Overrides.Keys | Sort-Object) { + if (-not $output.Contains($propertyName)) { + $output[$propertyName] = $Overrides[$propertyName] + } + } + + return [pscustomobject]$output +} + +function Set-JsonFile { + <# + .SYNOPSIS + Writes an object to a JSON file with UTF-8 encoding. + .DESCRIPTION + Serializes Content to JSON and writes to Path, creating parent + directories as needed. + .PARAMETER Path + Destination file path. + .PARAMETER Content + Object to serialize. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter(Mandatory = $true)] + [object]$Content + ) + + $parent = Split-Path -Path $Path -Parent + if (-not (Test-Path -Path $parent)) { + New-Item -Path $parent -ItemType Directory -Force | Out-Null + } + + $json = $Content | ConvertTo-Json -Depth 30 + Set-Content -Path $Path -Value $json -Encoding utf8NoBOM +} + +function Remove-StaleGeneratedFiles { + <# + .SYNOPSIS + Removes generated persona package files that are no longer expected. + .DESCRIPTION + Scans extension/ for package.*.json files and removes any not in the + expected set, keeping the directory clean of orphaned persona templates. + .PARAMETER RepoRoot + Repository root path. + .PARAMETER ExpectedFiles + Array of absolute paths that should be retained. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$RepoRoot, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [string[]]$ExpectedFiles + ) + + $expected = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($file in $ExpectedFiles) { + $null = $expected.Add([System.IO.Path]::GetFullPath($file)) + } + + $extensionDir = Join-Path $RepoRoot 'extension' + Get-ChildItem -Path $extensionDir -Filter 'package.*.json' -File | ForEach-Object { + $fullPath = [System.IO.Path]::GetFullPath($_.FullName) + if (-not $expected.Contains($fullPath)) { + Remove-Item -Path $_.FullName -Force + } + } +} + +function Invoke-ExtensionCollectionsGeneration { + <# + .SYNOPSIS + Generates persona package files from root collection manifests. + .DESCRIPTION + Reads the package template and each collections/*.collection.yml file, + producing extension/package.json (for hve-core-all) and + extension/package.{id}.json for every other collection. Stale persona + files are removed. + .PARAMETER RepoRoot + Repository root path containing collections/ and extension/templates/. + .OUTPUTS + [string[]] Array of generated file paths. + #> + [CmdletBinding()] + [OutputType([string[]])] + param( + [Parameter(Mandatory = $true)] + [string]$RepoRoot + ) + + $collectionsDir = Join-Path $RepoRoot 'collections' + $templatesDir = Join-Path $RepoRoot 'extension/templates' + + $packageTemplatePath = Join-Path $templatesDir 'package.template.json' + + if (-not (Test-Path $packageTemplatePath)) { + throw "Package template not found: $packageTemplatePath" + } + + if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) { + throw "Required module 'PowerShell-Yaml' is not installed." + } + + Import-Module PowerShell-Yaml -ErrorAction Stop + + $packageTemplate = Get-Content -Path $packageTemplatePath -Raw | ConvertFrom-Json + + $collectionFiles = Get-ChildItem -Path $collectionsDir -Filter '*.collection.yml' -File | Sort-Object Name + if ($collectionFiles.Count -eq 0) { + throw "No root collection files found in $collectionsDir" + } + + $expectedFiles = @() + + foreach ($collectionFile in $collectionFiles) { + $collection = ConvertFrom-Yaml -Yaml (Get-Content -Path $collectionFile.FullName -Raw) + if ($collection -isnot [hashtable]) { + throw "Collection manifest must be a hashtable: $($collectionFile.FullName)" + } + + $collectionId = [string]$collection.id + if ([string]::IsNullOrWhiteSpace($collectionId)) { + throw "Collection id is required: $($collectionFile.FullName)" + } + + $collectionDescription = if ($collection.ContainsKey('description')) { [string]$collection.description } else { [string]$packageTemplate.description } + + $extensionName = if ($collectionId -eq 'hve-core-all') { [string]$packageTemplate.name } else { "hve-$collectionId" } + $extensionDisplayName = if ($collectionId -eq 'hve-core-all') { + [string]$packageTemplate.displayName + } + else { + Get-CollectionDisplayName -CollectionManifest $collection -DefaultValue ([string]$packageTemplate.displayName) + } + + $packageTemplateOutput = Copy-TemplateWithOverrides -Template $packageTemplate -Overrides @{ + name = $extensionName + displayName = $extensionDisplayName + description = $collectionDescription + } + + $packagePath = if ($collectionId -eq 'hve-core-all') { + Join-Path $RepoRoot 'extension/package.json' + } + else { + Join-Path $RepoRoot "extension/package.$collectionId.json" + } + + Set-JsonFile -Path $packagePath -Content $packageTemplateOutput + $expectedFiles += $packagePath + } + + Remove-StaleGeneratedFiles -RepoRoot $RepoRoot -ExpectedFiles $expectedFiles + + return $expectedFiles +} + +#endregion Package Generation Functions + function Get-AllowedMaturities { <# .SYNOPSIS @@ -1112,7 +1349,18 @@ function Invoke-PrepareExtension { $GitHubDir = Join-Path $RepoRoot ".github" $PackageJsonPath = Join-Path $ExtensionDirectory "package.json" - # Validate required paths exist + # Generate persona package files from root collection manifests. + # This ensures extension/package.json and extension/package.*.json exist + # with the correct version from the template before any reads occur. + try { + $generated = Invoke-ExtensionCollectionsGeneration -RepoRoot $RepoRoot + Write-Host "Generated $($generated.Count) persona package file(s)" -ForegroundColor Green + } + catch { + return New-PrepareResult -Success $false -ErrorMessage "Package generation failed: $($_.Exception.Message)" + } + + # Validate required paths exist (package.json now guaranteed by generation) $pathValidation = Test-PathsExist -ExtensionDir $ExtensionDirectory ` -PackageJsonPath $PackageJsonPath ` -GitHubDir $GitHubDir diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index 217f54e9..a99c31c9 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -6,6 +6,231 @@ BeforeAll { . $PSScriptRoot/../../extension/Prepare-Extension.ps1 } +#region Package Generation Function Tests + +Describe 'Get-CollectionDisplayName' { + It 'Returns displayName when present' { + $manifest = @{ displayName = 'My Display Name'; name = 'fallback' } + $result = Get-CollectionDisplayName -CollectionManifest $manifest -DefaultValue 'default' + $result | Should -Be 'My Display Name' + } + + It 'Derives display name from name when displayName absent' { + $manifest = @{ name = 'Git Workflow' } + $result = Get-CollectionDisplayName -CollectionManifest $manifest -DefaultValue 'default' + $result | Should -Be 'HVE Core - Git Workflow' + } + + It 'Returns default when both displayName and name absent' { + $manifest = @{ id = 'test' } + $result = Get-CollectionDisplayName -CollectionManifest $manifest -DefaultValue 'Fallback' + $result | Should -Be 'Fallback' + } + + It 'Ignores whitespace-only displayName' { + $manifest = @{ displayName = ' '; name = 'valid' } + $result = Get-CollectionDisplayName -CollectionManifest $manifest -DefaultValue 'default' + $result | Should -Be 'HVE Core - valid' + } +} + +Describe 'Copy-TemplateWithOverrides' { + It 'Overrides existing properties' { + $template = [PSCustomObject]@{ name = 'original'; version = '1.0.0' } + $result = Copy-TemplateWithOverrides -Template $template -Overrides @{ name = 'overridden' } + $result.name | Should -Be 'overridden' + $result.version | Should -Be '1.0.0' + } + + It 'Preserves template property order' { + $template = [PSCustomObject]@{ a = '1'; b = '2'; c = '3' } + $result = Copy-TemplateWithOverrides -Template $template -Overrides @{ b = 'new' } + $names = @($result.PSObject.Properties.Name) + $names[0] | Should -Be 'a' + $names[1] | Should -Be 'b' + $names[2] | Should -Be 'c' + } + + It 'Appends new override keys not in template' { + $template = [PSCustomObject]@{ name = 'ext' } + $result = Copy-TemplateWithOverrides -Template $template -Overrides @{ name = 'ext'; extra = 'value' } + $result.extra | Should -Be 'value' + } + + It 'Returns PSCustomObject' { + $template = [PSCustomObject]@{ name = 'ext' } + $result = Copy-TemplateWithOverrides -Template $template -Overrides @{} + $result | Should -BeOfType [PSCustomObject] + } +} + +Describe 'Set-JsonFile' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Creates file with JSON content' { + $path = Join-Path $script:tempDir 'test.json' + Set-JsonFile -Path $path -Content @{ name = 'test'; version = '1.0.0' } + Test-Path $path | Should -BeTrue + $content = Get-Content -Path $path -Raw | ConvertFrom-Json + $content.name | Should -Be 'test' + } + + It 'Creates parent directories when missing' { + $path = Join-Path $script:tempDir 'nested/deep/test.json' + Set-JsonFile -Path $path -Content @{ key = 'value' } + Test-Path $path | Should -BeTrue + } +} + +Describe 'Remove-StaleGeneratedFiles' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + $script:extDir = Join-Path $script:tempDir 'extension' + New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Removes stale package.*.json files not in expected set' { + $keepFile = Join-Path $script:extDir 'package.rpi.json' + $staleFile = Join-Path $script:extDir 'package.obsolete.json' + '{}' | Set-Content -Path $keepFile + '{}' | Set-Content -Path $staleFile + + Remove-StaleGeneratedFiles -RepoRoot $script:tempDir -ExpectedFiles @($keepFile) + + Test-Path $keepFile | Should -BeTrue + Test-Path $staleFile | Should -BeFalse + } + + It 'Does not remove non-persona files' { + $regularFile = Join-Path $script:extDir 'README.md' + '# Test' | Set-Content -Path $regularFile + + Remove-StaleGeneratedFiles -RepoRoot $script:tempDir -ExpectedFiles @() + + Test-Path $regularFile | Should -BeTrue + } +} + +Describe 'Invoke-ExtensionCollectionsGeneration' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + + # Set up minimal repo structure + $collectionsDir = Join-Path $script:tempDir 'collections' + $templatesDir = Join-Path $script:tempDir 'extension/templates' + New-Item -ItemType Directory -Path $collectionsDir -Force | Out-Null + New-Item -ItemType Directory -Path $templatesDir -Force | Out-Null + + # Package template + @{ + name = 'hve-core' + displayName = 'HVE Core' + version = '2.0.0' + description = 'Default description' + publisher = 'test-pub' + engines = @{ vscode = '^1.80.0' } + contributes = @{} + } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $templatesDir 'package.template.json') + + # hve-core-all collection + @" +id: hve-core-all +name: hve-core +displayName: HVE Core +description: All artifacts +"@ | Set-Content -Path (Join-Path $collectionsDir 'hve-core-all.collection.yml') + + # rpi collection + @" +id: rpi +name: RPI Workflow +displayName: HVE Core - RPI Workflow +description: RPI workflow agents +"@ | Set-Content -Path (Join-Path $collectionsDir 'rpi.collection.yml') + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Generates package.json for hve-core-all' { + $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir + $pkgPath = Join-Path $script:tempDir 'extension/package.json' + Test-Path $pkgPath | Should -BeTrue + $pkg = Get-Content $pkgPath -Raw | ConvertFrom-Json + $pkg.name | Should -Be 'hve-core' + $pkg.version | Should -Be '2.0.0' + } + + It 'Generates persona package file for non-default collection' { + $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir + $pkgPath = Join-Path $script:tempDir 'extension/package.rpi.json' + Test-Path $pkgPath | Should -BeTrue + $pkg = Get-Content $pkgPath -Raw | ConvertFrom-Json + $pkg.name | Should -Be 'hve-rpi' + $pkg.displayName | Should -Be 'HVE Core - RPI Workflow' + } + + It 'Returns array of generated file paths' { + $result = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir + $result.Count | Should -Be 2 + } + + It 'Propagates version from template to all generated files' { + $result = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir + foreach ($file in $result) { + $pkg = Get-Content $file -Raw | ConvertFrom-Json + $pkg.version | Should -Be '2.0.0' + } + } + + It 'Removes stale persona files not matching current collections' { + $staleFile = Join-Path $script:tempDir 'extension/package.obsolete.json' + '{}' | Set-Content -Path $staleFile + + Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir + + Test-Path $staleFile | Should -BeFalse + } + + It 'Throws when package template is missing' { + $badRoot = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path (Join-Path $badRoot 'collections') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $badRoot 'extension/templates') -Force | Out-Null + @" +id: test +"@ | Set-Content -Path (Join-Path $badRoot 'collections/test.collection.yml') + + { Invoke-ExtensionCollectionsGeneration -RepoRoot $badRoot } | Should -Throw '*Package template not found*' + + Remove-Item -Path $badRoot -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Throws when no collection files exist' { + $emptyRoot = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'collections') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'extension/templates') -Force | Out-Null + @{ name = 'test'; version = '1.0.0' } | ConvertTo-Json | Set-Content -Path (Join-Path $emptyRoot 'extension/templates/package.template.json') + + { Invoke-ExtensionCollectionsGeneration -RepoRoot $emptyRoot } | Should -Throw '*No root collection files found*' + + Remove-Item -Path $emptyRoot -Recurse -Force -ErrorAction SilentlyContinue + } +} + +#endregion Package Generation Function Tests + Describe 'Get-AllowedMaturities' { It 'Returns only stable for Stable channel' { $result = Get-AllowedMaturities -Channel 'Stable' @@ -691,6 +916,31 @@ Describe 'Invoke-PrepareExtension' { } '@ | Set-Content -Path (Join-Path $script:extDir 'package.json') + # Create package template for generation + $script:templatesDir = Join-Path $script:extDir 'templates' + New-Item -ItemType Directory -Path $script:templatesDir -Force | Out-Null + @' +{ + "name": "hve-core", + "displayName": "HVE Core", + "version": "1.2.3", + "description": "Test extension", + "publisher": "test-pub", + "engines": { "vscode": "^1.80.0" }, + "contributes": {} +} +'@ | Set-Content -Path (Join-Path $script:templatesDir 'package.template.json') + + # Create collections directory with a minimal hve-core-all collection + $script:collectionsDir = Join-Path $script:tempDir 'collections' + New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null + @" +id: hve-core-all +name: hve-core +displayName: HVE Core +description: Test extension +"@ | Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') + # Create .github structure $script:ghDir = Join-Path $script:tempDir '.github' $script:agentsDir = Join-Path $script:ghDir 'agents' @@ -753,7 +1003,7 @@ applyTo: "**/*.ps1" -Channel 'Stable' $result.Success | Should -BeFalse - $result.ErrorMessage | Should -Match 'Required paths not found' + $result.ErrorMessage | Should -Not -BeNullOrEmpty } It 'Respects channel filtering' { @@ -886,64 +1136,47 @@ items: Test-Path (Join-Path $script:extDir 'CHANGELOG.md') | Should -BeTrue } - It 'Fails when package.json has invalid JSON' { - $badJsonDir = Join-Path $TestDrive 'bad-json-ext' - New-Item -ItemType Directory -Path $badJsonDir -Force | Out-Null - '{ invalid json }' | Set-Content -Path (Join-Path $badJsonDir 'package.json') - - # Create .github structure for this test - $badGhDir = Join-Path (Split-Path $badJsonDir -Parent) '.github' - New-Item -ItemType Directory -Path (Join-Path $badGhDir 'agents') -Force | Out-Null - - $result = Invoke-PrepareExtension ` - -ExtensionDirectory $badJsonDir ` - -RepoRoot (Split-Path $badJsonDir -Parent) ` - -Channel 'Stable' - - $result.Success | Should -BeFalse - $result.ErrorMessage | Should -Match 'Failed to parse package.json' - } - - It 'Fails when package.json missing version field' { - $noVersionDir = Join-Path $TestDrive 'no-version-ext' - New-Item -ItemType Directory -Path $noVersionDir -Force | Out-Null - '{"name": "test"}' | Set-Content -Path (Join-Path $noVersionDir 'package.json') - - # Create .github structure for this test - $noVersionGhDir = Join-Path (Split-Path $noVersionDir -Parent) '.github' - New-Item -ItemType Directory -Path (Join-Path $noVersionGhDir 'agents') -Force | Out-Null + It 'Fails when package template is missing' { + $badRoot = Join-Path $TestDrive 'bad-template-root' + $badExtDir = Join-Path $badRoot 'extension' + New-Item -ItemType Directory -Path $badExtDir -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $badRoot 'collections') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $badRoot '.github/agents') -Force | Out-Null + @" +id: test +"@ | Set-Content -Path (Join-Path $badRoot 'collections/test.collection.yml') $result = Invoke-PrepareExtension ` - -ExtensionDirectory $noVersionDir ` - -RepoRoot (Split-Path $noVersionDir -Parent) ` + -ExtensionDirectory $badExtDir ` + -RepoRoot $badRoot ` -Channel 'Stable' $result.Success | Should -BeFalse - $result.ErrorMessage | Should -Match "does not contain a 'version' field" + $result.ErrorMessage | Should -Match 'Package generation failed' } - It 'Fails when version format is invalid' { - $badVersionDir = Join-Path $TestDrive 'bad-version-ext' - New-Item -ItemType Directory -Path $badVersionDir -Force | Out-Null - '{"name": "test", "version": "invalid"}' | Set-Content -Path (Join-Path $badVersionDir 'package.json') - - # Create .github structure for this test - $badVersionGhDir = Join-Path (Split-Path $badVersionDir -Parent) '.github' - New-Item -ItemType Directory -Path (Join-Path $badVersionGhDir 'agents') -Force | Out-Null + It 'Fails when no collection YAML files exist' { + $emptyRoot = Join-Path $TestDrive 'empty-collections-root' + $emptyExtDir = Join-Path $emptyRoot 'extension' + New-Item -ItemType Directory -Path $emptyExtDir -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'collections') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'extension/templates') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $emptyRoot '.github/agents') -Force | Out-Null + @{ name = 'test'; version = '1.0.0' } | ConvertTo-Json | Set-Content -Path (Join-Path $emptyRoot 'extension/templates/package.template.json') $result = Invoke-PrepareExtension ` - -ExtensionDirectory $badVersionDir ` - -RepoRoot (Split-Path $badVersionDir -Parent) ` + -ExtensionDirectory $emptyExtDir ` + -RepoRoot $emptyRoot ` -Channel 'Stable' $result.Success | Should -BeFalse - $result.ErrorMessage | Should -Match 'Invalid version format' + $result.ErrorMessage | Should -Match 'Package generation failed' } Context 'Persona template copy' { BeforeAll { - # Developer collection manifest - $script:devCollectionPath = Join-Path $script:tempDir 'developer.collection.yml' + # Developer collection manifest (in collections/ for generation) + $script:devCollectionYaml = Join-Path $script:collectionsDir 'developer.collection.yml' @" id: developer name: hve-developer @@ -951,7 +1184,8 @@ displayName: HVE Core - Developer Edition description: Developer edition personas: - developer -"@ | Set-Content -Path $script:devCollectionPath +"@ | Set-Content -Path $script:devCollectionYaml + $script:devCollectionPath = $script:devCollectionYaml # hve-core-all collection manifest (default) $script:allCollectionPath = Join-Path $script:tempDir 'hve-core-all.collection.yml' @@ -975,23 +1209,10 @@ personas: - nonexistent "@ | Set-Content -Path $script:missingCollectionPath - # Persona template for developer collection - @' -{ - "name": "hve-developer", - "version": "1.2.3", - "contributes": {} -} -'@ | Set-Content -Path (Join-Path $script:extDir 'package.developer.json') - - } - - BeforeEach { - $script:originalPackageJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw } AfterEach { - $script:originalPackageJson | Set-Content -Path (Join-Path $script:extDir 'package.json') + # Clean up backup files left by persona template copy $bakPath = Join-Path $script:extDir 'package.json.bak' if (Test-Path $bakPath) { Remove-Item -Path $bakPath -Force @@ -1006,8 +1227,9 @@ personas: -DryRun $result.Success | Should -BeTrue - $currentContent = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw - $currentContent | Should -Be $script:originalPackageJson + # package.json should contain the generated hve-core-all content (not a persona template) + $currentJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json + $currentJson.name | Should -Be 'hve-core' Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse } @@ -1059,15 +1281,16 @@ personas: $result.Success | Should -BeTrue $bakPath = Join-Path $script:extDir 'package.json.bak' Test-Path $bakPath | Should -BeTrue - $bakContent = Get-Content -Path $bakPath -Raw - $bakContent | Should -Be $script:originalPackageJson + # Backup should contain the hve-core-all (canonical) generated content + $bakJson = Get-Content -Path $bakPath -Raw | ConvertFrom-Json + $bakJson.name | Should -Be 'hve-core' } } Context 'Collection maturity gating' { BeforeAll { - # Deprecated collection manifest - $script:deprecatedCollectionPath = Join-Path $script:tempDir 'deprecated.collection.yml' + # Deprecated collection in collections/ directory for generation + $script:deprecatedCollectionPath = Join-Path $script:collectionsDir 'deprecated-coll.collection.yml' @" id: deprecated-coll name: deprecated-ext @@ -1078,8 +1301,8 @@ personas: maturity: deprecated "@ | Set-Content -Path $script:deprecatedCollectionPath - # Experimental collection manifest - $script:experimentalCollectionPath = Join-Path $script:tempDir 'experimental.collection.yml' + # Experimental collection in collections/ directory for generation + $script:experimentalCollectionPath = Join-Path $script:collectionsDir 'experimental-coll.collection.yml' @" id: experimental-coll name: experimental-ext @@ -1089,15 +1312,6 @@ personas: - hve-core-all maturity: experimental "@ | Set-Content -Path $script:experimentalCollectionPath - - # Persona template for experimental collection - @' -{ - "name": "experimental-ext", - "version": "1.2.3", - "contributes": {} -} -'@ | Set-Content -Path (Join-Path $script:extDir 'package.experimental-coll.json') } It 'Returns early success for deprecated collection on Stable channel' { From 483b512de93199d78e0d061650cfb47c16a6c155 Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 22:29:59 -0800 Subject: [PATCH 50/62] feat(extension): add README generation for persona collections and update .gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - implement README generation from collection manifests - include new README template for consistent formatting - update .gitignore to exclude README files ๐Ÿ”ง - Generated by Copilot --- .gitignore | 2 + collections/ado.collection.md | 8 + collections/coding-standards.collection.md | 9 + collections/data-science.collection.md | 8 + collections/git.collection.md | 8 + collections/github.collection.md | 8 + collections/hve-core-all.collection.md | 3 + collections/project-planning.collection.md | 9 + collections/prompt-engineering.collection.md | 7 + collections/rpi.collection.md | 9 + collections/security-planning.collection.md | 8 + extension/README.developer.md | 109 ---------- extension/README.md | 129 ------------ extension/templates/README.template.md | 51 +++++ scripts/extension/Prepare-Extension.ps1 | 192 ++++++++++++++++++ .../extension/Prepare-Extension.Tests.ps1 | 188 +++++++++++++++++ 16 files changed, 510 insertions(+), 238 deletions(-) create mode 100644 collections/ado.collection.md create mode 100644 collections/coding-standards.collection.md create mode 100644 collections/data-science.collection.md create mode 100644 collections/git.collection.md create mode 100644 collections/github.collection.md create mode 100644 collections/hve-core-all.collection.md create mode 100644 collections/project-planning.collection.md create mode 100644 collections/prompt-engineering.collection.md create mode 100644 collections/rpi.collection.md create mode 100644 collections/security-planning.collection.md delete mode 100644 extension/README.developer.md delete mode 100644 extension/README.md create mode 100644 extension/templates/README.template.md diff --git a/.gitignore b/.gitignore index d2540f3f..2586e4a1 100644 --- a/.gitignore +++ b/.gitignore @@ -421,6 +421,8 @@ extension/CHANGELOG.md extension/package.json extension/package.*.json extension/package.json.bak +extension/README.md +extension/README.*.md !extension/templates/package.template.json # Windows Installer files from build outputs diff --git a/collections/ado.collection.md b/collections/ado.collection.md new file mode 100644 index 00000000..c72530f3 --- /dev/null +++ b/collections/ado.collection.md @@ -0,0 +1,8 @@ +Manage Azure DevOps work items, monitor builds, create pull requests, and convert requirements documents into structured work item hierarchies โ€” all from within VS Code. + +This collection includes agents and prompts for: + +- **Work Item Management** โ€” Discover, create, update, and plan work items across ADO projects +- **Build Monitoring** โ€” Query build status, review logs, and diagnose failures +- **Pull Request Creation** โ€” Generate PRs with linked work items and reviewer identification +- **PRD-to-Work-Item Conversion** โ€” Transform Product Requirements Documents into ADO feature/user-story/task hierarchies diff --git a/collections/coding-standards.collection.md b/collections/coding-standards.collection.md new file mode 100644 index 00000000..0e612b6a --- /dev/null +++ b/collections/coding-standards.collection.md @@ -0,0 +1,9 @@ +Enforce language-specific coding conventions and best practices across your projects. This collection provides instructions for bash, Bicep, C#, Python, and Terraform that are automatically applied based on file patterns. + +This collection includes instructions for: + +- **Bash** โ€” Shell scripting conventions and best practices +- **Bicep** โ€” Infrastructure as code implementation standards +- **C#** โ€” Code and test conventions including nullable reference types, async patterns, and xUnit testing +- **Python** โ€” Scripting implementation with type hints, docstrings, and uv project management +- **Terraform** โ€” Infrastructure as code with provider configuration and module structure diff --git a/collections/data-science.collection.md b/collections/data-science.collection.md new file mode 100644 index 00000000..fd95a6db --- /dev/null +++ b/collections/data-science.collection.md @@ -0,0 +1,8 @@ +Generate data specifications, Jupyter notebooks, and Streamlit dashboards from natural language descriptions. This collection includes specialized agents for data science workflows in Python. + +This collection includes agents for: + +- **Data Specification Generation** โ€” Create structured data schemas and specifications from requirements +- **Jupyter Notebook Generation** โ€” Build data analysis notebooks with visualizations and documentation +- **Streamlit Dashboard Generation** โ€” Create interactive dashboards from data sources +- **Dashboard Testing** โ€” Comprehensive test suites for Streamlit applications diff --git a/collections/git.collection.md b/collections/git.collection.md new file mode 100644 index 00000000..df602e1e --- /dev/null +++ b/collections/git.collection.md @@ -0,0 +1,8 @@ +Streamline Git operations with agents and prompts for commit messages, merge workflows, repository setup, and pull request management. + +This collection includes prompts for: + +- **Commit Messages** โ€” Generate conventional commit messages following project standards +- **Merge Operations** โ€” Handle merges, rebases, and conflict resolution workflows +- **Repository Setup** โ€” Initialize repositories with recommended configuration +- **Pull Requests** โ€” Create and manage pull requests with linked context diff --git a/collections/github.collection.md b/collections/github.collection.md new file mode 100644 index 00000000..25a62557 --- /dev/null +++ b/collections/github.collection.md @@ -0,0 +1,8 @@ +Manage GitHub issue backlogs with agents for discovery, triage, sprint planning, and execution. This collection brings structured backlog management workflows directly into VS Code. + +This collection includes agents and prompts for: + +- **Issue Discovery** โ€” Find and analyze issues across repositories with duplicate detection +- **Triage** โ€” Automated label suggestion, milestone assignment, and priority assessment +- **Sprint Planning** โ€” Organize issues into sprints with effort estimation +- **Backlog Execution** โ€” Execute planned operations against issue backlogs diff --git a/collections/hve-core-all.collection.md b/collections/hve-core-all.collection.md new file mode 100644 index 00000000..cdb96315 --- /dev/null +++ b/collections/hve-core-all.collection.md @@ -0,0 +1,3 @@ +HVE Core provides the complete collection of AI chat agents, prompts, instructions, and skills for VS Code with GitHub Copilot. This edition includes every artifact across all domains โ€” development workflows, architecture, Azure DevOps, data science, security, and more. + +Use this edition when you want access to everything without choosing a focused persona. diff --git a/collections/project-planning.collection.md b/collections/project-planning.collection.md new file mode 100644 index 00000000..cd29641d --- /dev/null +++ b/collections/project-planning.collection.md @@ -0,0 +1,9 @@ +Create architecture decision records, requirements documents, diagrams, and maintain documentation โ€” all through guided AI workflows. + +This collection includes agents for: + +- **Architecture Decision Records** โ€” Create structured ADRs with solution comparison matrices +- **Business Requirements Documents** โ€” Build BRDs through guided Q&A sessions +- **Product Requirements Documents** โ€” Build PRDs with stakeholder-driven refinement +- **Architecture Diagrams** โ€” Generate ASCII-art architecture diagrams from descriptions +- **Documentation Operations** โ€” Maintain and update existing documentation diff --git a/collections/prompt-engineering.collection.md b/collections/prompt-engineering.collection.md new file mode 100644 index 00000000..bbb7c76b --- /dev/null +++ b/collections/prompt-engineering.collection.md @@ -0,0 +1,7 @@ +Analyze, build, and refactor AI prompts, agents, and instructions with specialized tooling. This collection is designed for teams authoring and maintaining AI artifact libraries. + +This collection includes agents and prompts for: + +- **Prompt Analysis** โ€” Evaluate existing prompts for effectiveness and identify improvements +- **Prompt Building** โ€” Create new prompts following authoring standards and quality criteria +- **Prompt Refactoring** โ€” Restructure and improve existing prompts without changing intent diff --git a/collections/rpi.collection.md b/collections/rpi.collection.md new file mode 100644 index 00000000..b23ff28f --- /dev/null +++ b/collections/rpi.collection.md @@ -0,0 +1,9 @@ +Complete complex tasks through a structured five-phase workflow: Research, Plan, Implement, Review, and Discover. The RPI workflow dispatches specialized agents that collaborate autonomously to deliver well-researched, planned, and validated implementations. + +This collection includes agents for: + +- **RPI Agent** โ€” Autonomous orchestrator that drives the full five-phase workflow +- **Task Researcher** โ€” Gathers context, discovers patterns, and produces research documents +- **Task Planner** โ€” Creates detailed implementation plans from research findings +- **Task Implementor** โ€” Executes plans with progressive tracking and change records +- **Task Reviewer** โ€” Validates implementations against plans and project conventions diff --git a/collections/security-planning.collection.md b/collections/security-planning.collection.md new file mode 100644 index 00000000..c45f0fee --- /dev/null +++ b/collections/security-planning.collection.md @@ -0,0 +1,8 @@ +Create comprehensive security plans, incident response procedures, and risk assessments for cloud and hybrid environments. + +This collection includes agents and prompts for: + +- **Security Plan Creation** โ€” Generate threat models and security architecture documents +- **Incident Response** โ€” Build incident response runbooks and playbooks +- **Risk Assessment** โ€” Evaluate security risks with structured assessment frameworks +- **Root Cause Analysis** โ€” Structured RCA templates and guided analysis workflows diff --git a/extension/README.developer.md b/extension/README.developer.md deleted file mode 100644 index f3d112c9..00000000 --- a/extension/README.developer.md +++ /dev/null @@ -1,109 +0,0 @@ -# HVE Core - Developer Edition - -> AI-powered coding agents and prompts curated for software engineers - -HVE Core - Developer Edition provides a focused collection of AI chat agents, prompts, and instructions designed for software engineers working in VS Code with GitHub Copilot. This edition includes the RPI (Research-Plan-Implement) workflow and supporting development tools. - -## Features - -### ๐Ÿค– Chat Agents - -| Agent | Description | -|----------------------|--------------------------------------------------------------------------------------------------------------------| -| **memory** | Conversation memory persistence for session continuity | -| **rpi-agent** | Autonomous RPI orchestrator dispatching task agents through Research, Plan, Implement, Review, and Discover phases | -| **task-implementor** | Executes implementation plans with progressive tracking and change records | -| **task-planner** | Implementation planner for creating actionable implementation plans | -| **task-researcher** | Task research specialist for comprehensive project analysis | -| **task-reviewer** | Reviews completed implementation work for accuracy, completeness, and convention compliance | - -### ๐Ÿ“ Prompts - -| Prompt | Description | -|--------------------|---------------------------------------------------------------------------------------| -| **checkpoint** | Save or restore conversation context using memory files | -| **rpi** | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks | -| **task-implement** | Locates and executes implementation plans using task-implementor mode | -| **task-plan** | Initiates implementation planning based on user context or research documents | -| **task-research** | Initiates research for implementation planning based on user requirements | -| **task-review** | Initiates implementation review based on user context or automatic artifact discovery | - -### ๐Ÿ“š Instructions - -| Instruction | Description | -|--------------------|------------------------------------------------------------------| -| **commit-message** | Required instructions for creating all commit messages | -| **markdown** | Required instructions for creating or editing any Markdown files | - -### โšก Skills - -| Skill | Description | -|------------------|-----------------------------------------------------------| -| **video-to-gif** | Video-to-GIF conversion with FFmpeg two-pass optimization | - -## Getting Started - -After installing this extension, the chat agents will be available in GitHub Copilot Chat. You can: - -1. **Use custom agents** by selecting the custom agent from the agent picker drop-down list in Copilot Chat -2. **Apply prompts** through the Copilot Chat interface -3. **Reference instructions**: they're automatically applied based on file patterns - -### Post-Installation Setup - -Some chat agents create workflow artifacts in your project directory. See the [installation guide](https://github.com/microsoft/hve-core/blob/main/docs/getting-started/install.md#post-installation-update-your-gitignore) for recommended `.gitignore` configuration and other setup details. - -## Usage Examples - -### Using Chat Agents - -```plaintext -rpi-agent help me research and implement this feature end-to-end -task-planner help me break down this feature into implementable tasks -task-researcher investigate the best approach for adding authentication -``` - -### Applying Prompts - -Prompts are available in the Copilot Chat prompt picker and can be used to generate consistent, high-quality outputs for common tasks. - -## Pre-release Channel - -HVE Core offers two installation channels: - -| Channel | Description | Maturity Levels | -|-------------|---------------------------------------------------------|-------------------------------------| -| Stable | Production-ready artifacts only | `stable` | -| Pre-release | Early access to new features and experimental artifacts | `stable`, `preview`, `experimental` | - -To install the pre-release version, select **Install Pre-Release Version** from the extension page in VS Code, or use the Extensions view and switch to the pre-release channel. - -For more details on maturity levels and the release process, see the [contributing documentation](https://github.com/microsoft/hve-core/blob/main/docs/contributing/release-process.md#extension-channels-and-maturity). - -## Requirements - -- VS Code version 1.106.1 or higher -- GitHub Copilot extension - -## Full Edition - -Looking for more agents covering architecture, documentation, Azure DevOps, data science, and security? Check out the full [HVE Core](https://marketplace.visualstudio.com/items?itemName=ise-hve-essentials.hve-core) extension. - -## License - -MIT License - see [LICENSE](LICENSE) for details - -## Support - -For issues, questions, or contributions, please visit the [GitHub repository](https://github.com/microsoft/hve-core). - ---- - -Brought to you by Microsoft ISE HVE Essentials - ---- - - -*๐Ÿค– Crafted with precision by โœจCopilot following brilliant human instruction, -then carefully refined by our team of discerning human reviewers.* - diff --git a/extension/README.md b/extension/README.md deleted file mode 100644 index b4ca6e6a..00000000 --- a/extension/README.md +++ /dev/null @@ -1,129 +0,0 @@ -# HVE Core Extension - -> AI-powered chat agents, prompts, and instructions for hybrid virtual environments - -HVE Core provides a comprehensive collection of specialized AI chat agents, prompts, and instructions designed to accelerate development workflows in VS Code with GitHub Copilot. - -## Features - -### ๐Ÿค– Chat Agents - -Specialized AI assistants for specific development tasks: - -#### Development Workflow - -- **github-backlog-manager** - Consolidated GitHub backlog management with community interaction -- **pr-review** - Comprehensive pull request review assistant -- **rpi-agent** - Professional evidence-backed agent with structured subagent delegation for research, codebase discovery, and complex tasks -- **task-implementor** - Implement tasks from detailed plans -- **task-planner** - Plan and break down complex tasks -- **task-researcher** - Research technical solutions and approaches -- **task-reviewer** - Validate implementation against research and plan specifications - -#### Architecture & Documentation - -- **adr-creation** - Create Architecture Decision Records -- **arch-diagram-builder** - Build high-quality ASCII-art architecture diagrams -- **brd-builder** - Build Business Requirements Documents with guided Q&A -- **doc-ops** - Documentation operations and maintenance -- **prd-builder** - Build Product Requirements Documents with guided Q&A -- **prompt-builder** - Build and optimize AI prompts -- **security-plan-creator** - Expert security architect for creating comprehensive cloud security plans - -#### Azure DevOps Integration - -- **ado-prd-to-wit** - Convert Product Requirements Documents to Azure DevOps work items - -#### Data Science & Visualization - -- **gen-data-spec** - Generate data specifications and schemas -- **gen-jupyter-notebook** - Generate Jupyter notebooks for data analysis -- **gen-streamlit-dashboard** - Generate Streamlit dashboards -- **test-streamlit-dashboard** - Comprehensive testing of Streamlit dashboards - -#### Utility - -- **hve-core-installer** - Decision-driven HVE-Core installation with multiple methods -- **memory** - Persist repository facts for future task assistance - -### ๐Ÿ“ Prompts - -Reusable prompt templates for common workflows: - -- **Git Operations** - Commit messages, merges, setup, and pull requests -- **GitHub Workflows** - Issue creation and management -- **Azure DevOps** - PR creation, build info, and work item management - -### ๐Ÿ“š Instructions - -Best practice guidelines for: - -- **Languages** - Bash, Python, C#, Bicep -- **Git & Version Control** - Commit messages, merge operations -- **Documentation** - Markdown formatting -- **Azure DevOps** - Work item management and PR workflows -- **Task Management** - Implementation tracking and planning -- **Project Management** - UV projects and dependencies - -## Getting Started - -After installing this extension, the chat agents will be available in GitHub Copilot Chat. You can: - -1. **Use custom agents** by selecting the custom agent from the agent picker drop-down list in Copilot Chat -2. **Apply prompts** through the Copilot Chat interface -3. **Reference instructions** - They're automatically applied based on file patterns - -### Post-Installation Setup - -Some chat agents create workflow artifacts in your project directory. See the [installation guide](https://github.com/microsoft/hve-core/blob/main/docs/getting-started/install.md#post-installation-update-your-gitignore) for recommended `.gitignore` configuration and other setup details. - -## Usage Examples - -### Using Chat Agents - -```plaintext -task-planner help me break down this feature into implementable tasks -pr-review review this pull request for security issues -adr-creation create an ADR for our new microservice architecture -``` - -### Applying Prompts - -Prompts are available in the Copilot Chat prompt picker and can be used to generate consistent, high-quality outputs for common tasks. - -## Pre-release Channel - -HVE Core offers two installation channels: - -| Channel | Description | Maturity Levels | -|-------------|---------------------------------------------------------|-------------------------------------| -| Stable | Production-ready artifacts only | `stable` | -| Pre-release | Early access to new features and experimental artifacts | `stable`, `preview`, `experimental` | - -To install the pre-release version, select **Install Pre-Release Version** from the extension page in VS Code, or use the Extensions view and switch to the pre-release channel. - -For more details on maturity levels and the release process, see the [contributing documentation](https://github.com/microsoft/hve-core/blob/main/docs/contributing/release-process.md#extension-channels-and-maturity). - -## Requirements - -- VS Code version 1.106.1 or higher -- GitHub Copilot extension - -## License - -MIT License - see [LICENSE](LICENSE) for details - -## Support - -For issues, questions, or contributions, please visit the [GitHub repository](https://github.com/microsoft/hve-core). - ---- - -Brought to you by Microsoft ISE HVE Essentials - ---- - - -*๐Ÿค– Crafted with precision by โœจCopilot following brilliant human instruction, -then carefully refined by our team of discerning human reviewers.* - diff --git a/extension/templates/README.template.md b/extension/templates/README.template.md new file mode 100644 index 00000000..84f4f3dd --- /dev/null +++ b/extension/templates/README.template.md @@ -0,0 +1,51 @@ +# {{DISPLAY_NAME}} + +> {{DESCRIPTION}} + +{{BODY}} + +## Included Artifacts + +{{ARTIFACTS}} + +## Getting Started + +After installing this extension, the chat agents are available in GitHub Copilot Chat: + +1. **Use custom agents** by selecting the custom agent from the agent picker drop-down list in Copilot Chat +2. **Apply prompts** through the Copilot Chat interface +3. **Reference instructions** โ€” they are automatically applied based on file patterns + +### Post-Installation Setup + +Some chat agents create workflow artifacts in your project directory. See the [installation guide](https://github.com/microsoft/hve-core/blob/main/docs/getting-started/install.md#post-installation-update-your-gitignore) for recommended `.gitignore` configuration and other setup details. + +## Pre-release Channel + +HVE Core offers two installation channels: + +| Channel | Description | Maturity Levels | +|-------------|---------------------------------------------------------|-------------------------------------| +| Stable | Production-ready artifacts only | `stable` | +| Pre-release | Early access to new features and experimental artifacts | `stable`, `preview`, `experimental` | + +To install the pre-release version, select **Install Pre-Release Version** from the extension page in VS Code. + +{{FULL_EDITION}} + +## Requirements + +- VS Code version 1.106.1 or higher +- GitHub Copilot extension + +## License + +MIT License - see [LICENSE](LICENSE) for details + +## Support + +For issues, questions, or contributions, visit the [GitHub repository](https://github.com/microsoft/hve-core). + +--- + +Brought to you by Microsoft ISE HVE Essentials diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index f835c913..dfe62cd9 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -297,9 +297,201 @@ function Invoke-ExtensionCollectionsGeneration { Remove-StaleGeneratedFiles -RepoRoot $RepoRoot -ExpectedFiles $expectedFiles + # Generate README files for each persona collection + $readmeTemplatePath = Join-Path $templatesDir 'README.template.md' + foreach ($collectionFile in $collectionFiles) { + $collection = ConvertFrom-Yaml -Yaml (Get-Content -Path $collectionFile.FullName -Raw) + $collectionId = [string]$collection.id + + $collectionMdPath = Join-Path $collectionsDir "$collectionId.collection.md" + if (-not (Test-Path $collectionMdPath)) { + continue + } + + $readmePath = if ($collectionId -eq 'hve-core-all') { + Join-Path $RepoRoot 'extension/README.md' + } + else { + Join-Path $RepoRoot "extension/README.$collectionId.md" + } + + New-CollectionReadme -Collection $collection -CollectionMdPath $collectionMdPath -TemplatePath $readmeTemplatePath -RepoRoot $RepoRoot -OutputPath $readmePath + } + return $expectedFiles } +function Get-ArtifactDescription { + <# + .SYNOPSIS + Reads the description from an artifact file's YAML frontmatter. + .DESCRIPTION + Parses the YAML frontmatter block at the top of a markdown file and + returns the description field value. Returns an empty string when the + file is missing, has no frontmatter, or lacks a description field. + Strips the common " - Brought to you by microsoft/hve-core" suffix. + .PARAMETER FilePath + Absolute path to the artifact markdown file. + .OUTPUTS + [string] Description text, or empty string if unavailable. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [string]$FilePath + ) + + if (-not (Test-Path $FilePath)) { + return '' + } + + $content = Get-Content -Path $FilePath -Raw + if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') { + $yamlBlock = $Matches[1] + try { + $frontmatter = ConvertFrom-Yaml -Yaml $yamlBlock + if ($frontmatter -is [hashtable] -and $frontmatter.ContainsKey('description')) { + $desc = [string]$frontmatter.description + # Strip the common branding suffix + $desc = $desc -replace '\s*-\s*Brought to you by microsoft/hve-core$', '' + return $desc.Trim() + } + } + catch { + Write-Verbose "Failed to parse frontmatter from $FilePath`: $_" + } + } + + return '' +} + +function New-CollectionReadme { + <# + .SYNOPSIS + Generates a README.md for an extension collection from a template. + .DESCRIPTION + Reads a README template and replaces placeholder tokens with collection + metadata, hand-authored body content, and auto-generated artifact tables + with descriptions read from each artifact's YAML frontmatter. + Tokens: {{DISPLAY_NAME}}, {{DESCRIPTION}}, {{BODY}}, {{ARTIFACTS}}, + {{FULL_EDITION}}. + .PARAMETER Collection + Parsed collection manifest hashtable. + .PARAMETER CollectionMdPath + Path to the collection markdown body file. + .PARAMETER TemplatePath + Path to the README template file containing placeholder tokens. + .PARAMETER RepoRoot + Repository root path for resolving artifact file paths. + .PARAMETER OutputPath + Destination path for the generated README. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [hashtable]$Collection, + + [Parameter(Mandatory = $true)] + [string]$CollectionMdPath, + + [Parameter(Mandatory = $true)] + [string]$TemplatePath, + + [Parameter(Mandatory = $true)] + [string]$RepoRoot, + + [Parameter(Mandatory = $true)] + [string]$OutputPath + ) + + $collectionId = [string]$Collection.id + $displayName = if ($collectionId -eq 'hve-core-all') { + 'HVE Core' + } + else { + Get-CollectionDisplayName -CollectionManifest $Collection -DefaultValue "HVE Core - $collectionId" + } + $description = if ($Collection.ContainsKey('description')) { [string]$Collection.description } else { '' } + + $bodyContent = (Get-Content -Path $CollectionMdPath -Raw).Trim() + + # Collect artifacts with descriptions grouped by kind + $agents = @() + $prompts = @() + $instructions = @() + $skills = @() + + if ($Collection.ContainsKey('items')) { + foreach ($item in $Collection.items) { + if (-not $item.ContainsKey('kind') -or -not $item.ContainsKey('path')) { + continue + } + $kind = [string]$item.kind + $path = [string]$item.path + $artifactName = Get-CollectionArtifactKey -Kind $kind -Path $path + + # Resolve full file path for frontmatter reading + $resolvedPath = Join-Path $RepoRoot ($path -replace '^\./', '') + if ($kind -eq 'skill') { + $resolvedPath = Join-Path $resolvedPath 'SKILL.md' + } + $artifactDesc = Get-ArtifactDescription -FilePath $resolvedPath + + $entry = @{ Name = $artifactName; Description = $artifactDesc } + switch ($kind) { + 'agent' { $agents += $entry } + 'prompt' { $prompts += $entry } + 'instruction' { $instructions += $entry } + 'skill' { $skills += $entry } + } + } + } + + # Build markdown tables for each artifact kind + $artifactSections = [System.Text.StringBuilder]::new() + + foreach ($section in @( + @{ Title = 'Chat Agents'; Items = $agents }, + @{ Title = 'Prompts'; Items = $prompts }, + @{ Title = 'Instructions'; Items = $instructions }, + @{ Title = 'Skills'; Items = $skills } + )) { + if ($section.Items.Count -eq 0) { continue } + + $null = $artifactSections.AppendLine("### $($section.Title)") + $null = $artifactSections.AppendLine() + $null = $artifactSections.AppendLine('| Name | Description |') + $null = $artifactSections.AppendLine('|------|-------------|') + foreach ($entry in ($section.Items | Sort-Object { $_.Name })) { + $null = $artifactSections.AppendLine("| **$($entry.Name)** | $($entry.Description) |") + } + $null = $artifactSections.AppendLine() + } + + $fullEdition = if ($collectionId -ne 'hve-core-all') { + "## Full Edition`n`nLooking for more agents covering additional domains? Check out the full [HVE Core](https://marketplace.visualstudio.com/items?itemName=ise-hve-essentials.hve-core) extension." + } + else { + '' + } + + # Read template and replace tokens + $template = Get-Content -Path $TemplatePath -Raw + $readmeContent = $template ` + -replace '\{\{DISPLAY_NAME\}\}', $displayName ` + -replace '\{\{DESCRIPTION\}\}', $description ` + -replace '\{\{BODY\}\}', $bodyContent ` + -replace '\{\{ARTIFACTS\}\}', $artifactSections.ToString().TrimEnd() ` + -replace '\{\{FULL_EDITION\}\}', $fullEdition + + # Clean up blank lines left by empty token replacements + $readmeContent = $readmeContent -replace '(\r?\n){3,}', "`n`n" + $readmeContent = $readmeContent.TrimEnd() + "`n" + + Set-Content -Path $OutputPath -Value $readmeContent -Encoding utf8NoBOM -NoNewline +} + #endregion Package Generation Functions function Get-AllowedMaturities { diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index a99c31c9..aed0a6a4 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -229,6 +229,194 @@ id: test } } +Describe 'New-CollectionReadme' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null + + # Resolve the real template from the repo + $script:repoRoot = (Get-Item "$PSScriptRoot/../../..").FullName + $script:templatePath = Join-Path $script:repoRoot 'extension/templates/README.template.md' + + # Create mock artifact files with frontmatter descriptions + $agentsDir = Join-Path $script:tempDir '.github/agents' + $promptsDir = Join-Path $script:tempDir '.github/prompts' + $instrDir = Join-Path $script:tempDir '.github/instructions' + $skillsDir = Join-Path $script:tempDir '.github/skills/my-skill' + New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null + New-Item -ItemType Directory -Path $promptsDir -Force | Out-Null + New-Item -ItemType Directory -Path $instrDir -Force | Out-Null + New-Item -ItemType Directory -Path $skillsDir -Force | Out-Null + + @" +--- +description: "Alpha agent description" +--- +# Alpha +"@ | Set-Content -Path (Join-Path $agentsDir 'alpha.agent.md') + + @" +--- +description: "Zebra agent description" +--- +# Zebra +"@ | Set-Content -Path (Join-Path $agentsDir 'zebra.agent.md') + + @" +--- +description: "My prompt description" +--- +# Prompt +"@ | Set-Content -Path (Join-Path $promptsDir 'my-prompt.prompt.md') + + @" +--- +description: "My instruction description" +applyTo: "**/*.ps1" +--- +# Instruction +"@ | Set-Content -Path (Join-Path $instrDir 'my-instr.instructions.md') + + @" +--- +name: my-skill +description: "My skill description" +--- +# Skill +"@ | Set-Content -Path (Join-Path $skillsDir 'SKILL.md') + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Generates README with title and description from collection manifest' { + $collection = @{ + id = 'test-coll' + name = 'Test Collection' + description = 'A test collection for unit testing' + items = @() + } + $mdPath = Join-Path $script:tempDir 'test.collection.md' + 'Body content goes here.' | Set-Content -Path $mdPath + $outPath = Join-Path $script:tempDir 'README.test-coll.md' + + New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath + + $content = Get-Content -Path $outPath -Raw + $content | Should -Match '# HVE Core - Test Collection' + $content | Should -Match '> A test collection for unit testing' + $content | Should -Match 'Body content goes here' + } + + It 'Uses HVE Core as title for hve-core-all collection' { + $collection = @{ + id = 'hve-core-all' + name = 'HVE Core All' + description = 'Full bundle' + items = @() + } + $mdPath = Join-Path $script:tempDir 'all.collection.md' + 'All artifacts.' | Set-Content -Path $mdPath + $outPath = Join-Path $script:tempDir 'README.md' + + New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath + + $content = Get-Content -Path $outPath -Raw + $content | Should -Match '# HVE Core' + $content | Should -Not -Match '# HVE Core All' + } + + It 'Generates sorted artifact tables with descriptions grouped by kind' { + $collection = @{ + id = 'multi' + name = 'Multi' + description = 'Multi-artifact test' + items = @( + @{ kind = 'agent'; path = '.github/agents/zebra.agent.md' }, + @{ kind = 'agent'; path = '.github/agents/alpha.agent.md' }, + @{ kind = 'prompt'; path = '.github/prompts/my-prompt.prompt.md' }, + @{ kind = 'instruction'; path = '.github/instructions/my-instr.instructions.md' }, + @{ kind = 'skill'; path = '.github/skills/my-skill/' } + ) + } + $mdPath = Join-Path $script:tempDir 'multi.collection.md' + 'Test body.' | Set-Content -Path $mdPath + $outPath = Join-Path $script:tempDir 'README.multi.md' + + New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath + + $content = Get-Content -Path $outPath -Raw + $content | Should -Match '### Chat Agents' + $content | Should -Match '\| Name \| Description \|' + $content | Should -Match '\*\*alpha\*\*.*Alpha agent description' + $content | Should -Match '\*\*zebra\*\*.*Zebra agent description' + $content | Should -Match '### Prompts' + $content | Should -Match '\*\*my-prompt\*\*.*My prompt description' + $content | Should -Match '### Instructions' + $content | Should -Match '\*\*my-instr\*\*.*My instruction description' + $content | Should -Match '### Skills' + $content | Should -Match '\*\*my-skill\*\*.*My skill description' + } + + It 'Includes Full Edition link for persona collections' { + $collection = @{ + id = 'persona' + name = 'Persona' + description = 'Persona test' + items = @() + } + $mdPath = Join-Path $script:tempDir 'persona.collection.md' + 'Persona body.' | Set-Content -Path $mdPath + $outPath = Join-Path $script:tempDir 'README.persona.md' + + New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath + + $content = Get-Content -Path $outPath -Raw + $content | Should -Match '## Full Edition' + $content | Should -Match 'HVE Core.*extension' + } + + It 'Excludes Full Edition link for hve-core-all' { + $collection = @{ + id = 'hve-core-all' + name = 'All' + description = 'Full bundle' + items = @() + } + $mdPath = Join-Path $script:tempDir 'all2.collection.md' + 'All body.' | Set-Content -Path $mdPath + $outPath = Join-Path $script:tempDir 'README.all2.md' + + New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath + + $content = Get-Content -Path $outPath -Raw + $content | Should -Not -Match '## Full Edition' + } + + It 'Includes common footer sections' { + $collection = @{ + id = 'footer-test' + name = 'Footer' + description = 'Footer test' + items = @() + } + $mdPath = Join-Path $script:tempDir 'footer.collection.md' + 'Footer body.' | Set-Content -Path $mdPath + $outPath = Join-Path $script:tempDir 'README.footer.md' + + New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath + + $content = Get-Content -Path $outPath -Raw + $content | Should -Match '## Getting Started' + $content | Should -Match '## Pre-release Channel' + $content | Should -Match '## Requirements' + $content | Should -Match '## License' + $content | Should -Match '## Support' + $content | Should -Match 'Microsoft ISE HVE Essentials' + } +} + #endregion Package Generation Function Tests Describe 'Get-AllowedMaturities' { From c79b021a89eea4d90ac8fb2a62c31d024c03271b Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 22:50:07 -0800 Subject: [PATCH 51/62] chore(templates): remove outdated links and add shared resource directories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - delete obsolete markdown files for GitHub issue manager and workflows - create symlinks for shared directories in project planning, prompt engineering, and security planning - update PluginHelpers.psm1 to include new shared resource directories ๐Ÿ”— - Generated by Copilot --- plugins/ado/README.md | 74 ++++---- plugins/ado/docs/templates | 1 + plugins/ado/scripts/dev-tools | 1 + plugins/ado/scripts/lib | 1 + plugins/coding-standards/README.md | 64 +++---- plugins/coding-standards/docs/templates | 1 + plugins/coding-standards/scripts/dev-tools | 1 + plugins/coding-standards/scripts/lib | 1 + plugins/data-science/README.md | 62 +++---- plugins/data-science/docs/templates | 1 + plugins/data-science/scripts/dev-tools | 1 + plugins/data-science/scripts/lib | 1 + plugins/git/README.md | 64 +++---- plugins/git/docs/templates | 1 + plugins/git/scripts/dev-tools | 1 + plugins/git/scripts/lib | 1 + plugins/github/README.md | 72 ++++---- plugins/github/docs/templates | 1 + plugins/github/scripts/dev-tools | 1 + plugins/github/scripts/lib | 1 + plugins/hve-core-all/README.md | 160 +++++++++--------- .../agents/github-issue-manager.md | 1 - plugins/hve-core-all/docs/templates | 1 + .../hve-core-all/instructions/workflows.md | 1 - plugins/hve-core-all/scripts/dev-tools | 1 + plugins/hve-core-all/scripts/lib | 1 + plugins/project-planning/README.md | 62 +++---- plugins/project-planning/docs/templates | 1 + plugins/project-planning/scripts/dev-tools | 1 + plugins/project-planning/scripts/lib | 1 + plugins/prompt-engineering/README.md | 50 +++--- plugins/prompt-engineering/docs/templates | 1 + plugins/prompt-engineering/scripts/dev-tools | 1 + plugins/prompt-engineering/scripts/lib | 1 + plugins/rpi/README.md | 50 +++--- plugins/rpi/docs/templates | 1 + plugins/rpi/scripts/dev-tools | 1 + plugins/rpi/scripts/lib | 1 + plugins/security-planning/README.md | 56 +++--- plugins/security-planning/docs/templates | 1 + plugins/security-planning/scripts/dev-tools | 1 + plugins/security-planning/scripts/lib | 1 + scripts/plugins/Modules/PluginHelpers.psm1 | 24 +++ 43 files changed, 410 insertions(+), 360 deletions(-) create mode 120000 plugins/ado/docs/templates create mode 120000 plugins/ado/scripts/dev-tools create mode 120000 plugins/ado/scripts/lib create mode 120000 plugins/coding-standards/docs/templates create mode 120000 plugins/coding-standards/scripts/dev-tools create mode 120000 plugins/coding-standards/scripts/lib create mode 120000 plugins/data-science/docs/templates create mode 120000 plugins/data-science/scripts/dev-tools create mode 120000 plugins/data-science/scripts/lib create mode 120000 plugins/git/docs/templates create mode 120000 plugins/git/scripts/dev-tools create mode 120000 plugins/git/scripts/lib create mode 120000 plugins/github/docs/templates create mode 120000 plugins/github/scripts/dev-tools create mode 120000 plugins/github/scripts/lib delete mode 120000 plugins/hve-core-all/agents/github-issue-manager.md create mode 120000 plugins/hve-core-all/docs/templates delete mode 120000 plugins/hve-core-all/instructions/workflows.md create mode 120000 plugins/hve-core-all/scripts/dev-tools create mode 120000 plugins/hve-core-all/scripts/lib create mode 120000 plugins/project-planning/docs/templates create mode 120000 plugins/project-planning/scripts/dev-tools create mode 120000 plugins/project-planning/scripts/lib create mode 120000 plugins/prompt-engineering/docs/templates create mode 120000 plugins/prompt-engineering/scripts/dev-tools create mode 120000 plugins/prompt-engineering/scripts/lib create mode 120000 plugins/rpi/docs/templates create mode 120000 plugins/rpi/scripts/dev-tools create mode 120000 plugins/rpi/scripts/lib create mode 120000 plugins/security-planning/docs/templates create mode 120000 plugins/security-planning/scripts/dev-tools create mode 120000 plugins/security-planning/scripts/lib diff --git a/plugins/ado/README.md b/plugins/ado/README.md index 00a5e172..7c81c9ec 100644 --- a/plugins/ado/README.md +++ b/plugins/ado/README.md @@ -11,49 +11,49 @@ copilot plugin install ado@hve-core ## Agents -| Agent | Description | -|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ado-prd-to-wit | Product Manager expert for analyzing PRDs and planning Azure DevOps work item hierarchies | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +| ----- | ----------- | +| ado-prd-to-wit | Product Manager expert for analyzing PRDs and planning Azure DevOps work item hierarchies | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -|---------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------| -| ado-create-pull-request | Generate pull request description, discover related work items, identify reviewers, and create Azure DevOps pull request with all linkages. | -| ado-get-build-info | Retrieve Azure DevOps build information for a Pull Request or specific Build Number. | -| ado-get-my-work-items | Retrieve user's current Azure DevOps work items and organize them into planning file definitions | -| ado-process-my-work-items-for-task-planning | Process retrieved work items for task planning and generate task-planning-logs.md handoff file | -| ado-update-wit-items | Prompt to update work items based on planning files | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +| ------- | ----------- | +| ado-create-pull-request | Generate pull request description, discover related work items, identify reviewers, and create Azure DevOps pull request with all linkages. | +| ado-get-build-info | Retrieve Azure DevOps build information for a Pull Request or specific Build Number. | +| ado-get-my-work-items | Retrieve user's current Azure DevOps work items and organize them into planning file definitions | +| ado-process-my-work-items-for-task-planning | Process retrieved work items for task planning and generate task-planning-logs.md handoff file | +| ado-update-wit-items | Prompt to update work items based on planning files | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | -| prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | -| ado-create-pull-request | Required protocol for creating Azure DevOps pull requests with work item discovery, reviewer identification, and automated linking. | -| ado-get-build-info | Required instructions for anything related to Azure Devops or ado build information including status, logs, or details from provided pullrequest (PR), build Id, or branch name. | -| ado-update-wit-items | Work item creation and update protocol using MCP ADO tools with handoff tracking | -| ado-wit-discovery | Protocol for discovering Azure DevOps work items via user assignment or artifact analysis with planning file output | -| ado-wit-planning | Reference specification for Azure DevOps work item planning files, templates, field definitions, and search protocols | +| Instruction | Description | +| ----------- | ----------- | +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | +| ado-create-pull-request | Required protocol for creating Azure DevOps pull requests with work item discovery, reviewer identification, and automated linking. | +| ado-get-build-info | Required instructions for anything related to Azure Devops or ado build information including status, logs, or details from provided pullrequest (PR), build Id, or branch name. | +| ado-update-wit-items | Work item creation and update protocol using MCP ADO tools with handoff tracking | +| ado-wit-discovery | Protocol for discovering Azure DevOps work items via user assignment or artifact analysis with planning file output | +| ado-wit-planning | Reference specification for Azure DevOps work item planning files, templates, field definitions, and search protocols | --- diff --git a/plugins/ado/docs/templates b/plugins/ado/docs/templates new file mode 120000 index 00000000..3c16d73f --- /dev/null +++ b/plugins/ado/docs/templates @@ -0,0 +1 @@ +../../../docs/templates \ No newline at end of file diff --git a/plugins/ado/scripts/dev-tools b/plugins/ado/scripts/dev-tools new file mode 120000 index 00000000..00d24071 --- /dev/null +++ b/plugins/ado/scripts/dev-tools @@ -0,0 +1 @@ +../../../scripts/dev-tools \ No newline at end of file diff --git a/plugins/ado/scripts/lib b/plugins/ado/scripts/lib new file mode 120000 index 00000000..4d903196 --- /dev/null +++ b/plugins/ado/scripts/lib @@ -0,0 +1 @@ +../../../scripts/lib \ No newline at end of file diff --git a/plugins/coding-standards/README.md b/plugins/coding-standards/README.md index b4738dcc..e1713d37 100644 --- a/plugins/coding-standards/README.md +++ b/plugins/coding-standards/README.md @@ -11,45 +11,45 @@ copilot plugin install coding-standards@hve-core ## Agents -| Agent | Description | -|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +| ----- | ----------- | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -|-----------------|------------------------------------------------------------------------------------------------------------------------------| -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +| ------- | ----------- | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Instruction | Description | +| ----------- | ----------- | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | -| bash | Instructions for bash script implementation - Brought to you by microsoft/edge-ai | -| bicep | Instructions for Bicep infrastructure as code implementation - Brought to you by microsoft/hve-core | -| csharp | Required instructions for C# (CSharp) research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | -| csharp-tests | Required instructions for C# (CSharp) test code research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | -| python-script | Instructions for Python scripting implementation - Brought to you by microsoft/hve-core | -| terraform | Instructions for Terraform infrastructure as code implementation - Brought to you by microsoft/hve-core | -| uv-projects | Create and manage Python virtual environments using uv commands | -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| bash | Instructions for bash script implementation - Brought to you by microsoft/edge-ai | +| bicep | Instructions for Bicep infrastructure as code implementation - Brought to you by microsoft/hve-core | +| csharp | Required instructions for C# (CSharp) research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | +| csharp-tests | Required instructions for C# (CSharp) test code research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | +| python-script | Instructions for Python scripting implementation - Brought to you by microsoft/hve-core | +| terraform | Instructions for Terraform infrastructure as code implementation - Brought to you by microsoft/hve-core | +| uv-projects | Create and manage Python virtual environments using uv commands | +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | --- diff --git a/plugins/coding-standards/docs/templates b/plugins/coding-standards/docs/templates new file mode 120000 index 00000000..3c16d73f --- /dev/null +++ b/plugins/coding-standards/docs/templates @@ -0,0 +1 @@ +../../../docs/templates \ No newline at end of file diff --git a/plugins/coding-standards/scripts/dev-tools b/plugins/coding-standards/scripts/dev-tools new file mode 120000 index 00000000..00d24071 --- /dev/null +++ b/plugins/coding-standards/scripts/dev-tools @@ -0,0 +1 @@ +../../../scripts/dev-tools \ No newline at end of file diff --git a/plugins/coding-standards/scripts/lib b/plugins/coding-standards/scripts/lib new file mode 120000 index 00000000..4d903196 --- /dev/null +++ b/plugins/coding-standards/scripts/lib @@ -0,0 +1 @@ +../../../scripts/lib \ No newline at end of file diff --git a/plugins/data-science/README.md b/plugins/data-science/README.md index ffbd7475..e271e55d 100644 --- a/plugins/data-science/README.md +++ b/plugins/data-science/README.md @@ -11,44 +11,44 @@ copilot plugin install data-science@hve-core ## Agents -| Agent | Description | -|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| gen-data-spec | Generate comprehensive data dictionaries, machine-readable data profiles, and objective summaries for downstream analysis (EDA notebooks, dashboards) through guided discovery | -| gen-jupyter-notebook | Create structured exploratory data analysis Jupyter notebooks from available data sources and generated data dictionaries | -| gen-streamlit-dashboard | Develop a multi-page Streamlit dashboard | -| test-streamlit-dashboard | Automated testing for Streamlit dashboards using Playwright with issue tracking and reporting | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +| ----- | ----------- | +| gen-data-spec | Generate comprehensive data dictionaries, machine-readable data profiles, and objective summaries for downstream analysis (EDA notebooks, dashboards) through guided discovery | +| gen-jupyter-notebook | Create structured exploratory data analysis Jupyter notebooks from available data sources and generated data dictionaries | +| gen-streamlit-dashboard | Develop a multi-page Streamlit dashboard | +| test-streamlit-dashboard | Automated testing for Streamlit dashboards using Playwright with issue tracking and reporting | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -|-----------------|------------------------------------------------------------------------------------------------------------------------------| -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +| ------- | ----------- | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| Instruction | Description | +| ----------- | ----------- | +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | -| python-script | Instructions for Python scripting implementation - Brought to you by microsoft/hve-core | -| uv-projects | Create and manage Python virtual environments using uv commands | +| python-script | Instructions for Python scripting implementation - Brought to you by microsoft/hve-core | +| uv-projects | Create and manage Python virtual environments using uv commands | --- diff --git a/plugins/data-science/docs/templates b/plugins/data-science/docs/templates new file mode 120000 index 00000000..3c16d73f --- /dev/null +++ b/plugins/data-science/docs/templates @@ -0,0 +1 @@ +../../../docs/templates \ No newline at end of file diff --git a/plugins/data-science/scripts/dev-tools b/plugins/data-science/scripts/dev-tools new file mode 120000 index 00000000..00d24071 --- /dev/null +++ b/plugins/data-science/scripts/dev-tools @@ -0,0 +1 @@ +../../../scripts/dev-tools \ No newline at end of file diff --git a/plugins/data-science/scripts/lib b/plugins/data-science/scripts/lib new file mode 120000 index 00000000..4d903196 --- /dev/null +++ b/plugins/data-science/scripts/lib @@ -0,0 +1 @@ +../../../scripts/lib \ No newline at end of file diff --git a/plugins/git/README.md b/plugins/git/README.md index 39067be9..633fd1bb 100644 --- a/plugins/git/README.md +++ b/plugins/git/README.md @@ -11,45 +11,45 @@ copilot plugin install git@hve-core ## Agents -| Agent | Description | -|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| pr-review | Comprehensive Pull Request review assistant ensuring code quality, security, and convention compliance - Brought to you by microsoft/hve-core | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +| ----- | ----------- | +| pr-review | Comprehensive Pull Request review assistant ensuring code quality, security, and convention compliance - Brought to you by microsoft/hve-core | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -|--------------------|------------------------------------------------------------------------------------------------------------------------------| -| git-commit-message | Generates a commit message following the commit-message.instructions.md rules based on all changes in the branch | -| git-commit | Stages all changes, generates a conventional commit message, shows it to the user, and commits using only git add/commit | -| git-merge | Coordinate Git merge, rebase, and rebase --onto workflows with consistent conflict handling. | -| git-setup | Interactive, verification-first Git configuration assistant (non-destructive) | -| pull-request | Provides prompt instructions for pull request (PR) generation - Brought to you by microsoft/edge-ai | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +| ------- | ----------- | +| git-commit-message | Generates a commit message following the commit-message.instructions.md rules based on all changes in the branch | +| git-commit | Stages all changes, generates a conventional commit message, shows it to the user, and commits using only git add/commit | +| git-merge | Coordinate Git merge, rebase, and rebase --onto workflows with consistent conflict handling. | +| git-setup | Interactive, verification-first Git configuration assistant (non-destructive) | +| pull-request | Provides prompt instructions for pull request (PR) generation - Brought to you by microsoft/edge-ai | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| Instruction | Description | +| ----------- | ----------- | +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | -| git-merge | Required protocol for Git merge, rebase, and rebase --onto workflows with conflict handling and stop controls. | +| git-merge | Required protocol for Git merge, rebase, and rebase --onto workflows with conflict handling and stop controls. | --- diff --git a/plugins/git/docs/templates b/plugins/git/docs/templates new file mode 120000 index 00000000..3c16d73f --- /dev/null +++ b/plugins/git/docs/templates @@ -0,0 +1 @@ +../../../docs/templates \ No newline at end of file diff --git a/plugins/git/scripts/dev-tools b/plugins/git/scripts/dev-tools new file mode 120000 index 00000000..00d24071 --- /dev/null +++ b/plugins/git/scripts/dev-tools @@ -0,0 +1 @@ +../../../scripts/dev-tools \ No newline at end of file diff --git a/plugins/git/scripts/lib b/plugins/git/scripts/lib new file mode 120000 index 00000000..4d903196 --- /dev/null +++ b/plugins/git/scripts/lib @@ -0,0 +1 @@ +../../../scripts/lib \ No newline at end of file diff --git a/plugins/github/README.md b/plugins/github/README.md index 10658693..ed220b5b 100644 --- a/plugins/github/README.md +++ b/plugins/github/README.md @@ -11,49 +11,49 @@ copilot plugin install github@hve-core ## Agents -| Agent | Description | -|------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| github-backlog-manager | Orchestrator agent for GitHub backlog management workflows including triage, discovery, sprint planning, and execution - Brought to you by microsoft/hve-core | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +| ----- | ----------- | +| github-backlog-manager | Orchestrator agent for GitHub backlog management workflows including triage, discovery, sprint planning, and execution - Brought to you by microsoft/hve-core | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| -| github-add-issue | Create a GitHub issue using discovered repository templates and conversational field collection | +| Command | Description | +| ------- | ----------- | +| github-add-issue | Create a GitHub issue using discovered repository templates and conversational field collection | | github-discover-issues | Discover GitHub issues through user-centric queries, artifact-driven analysis, or search-based exploration and produce planning files for review | -| github-triage-issues | Triage GitHub issues not yet triaged with automated label suggestions, milestone assignment, and duplicate detection | -| github-execute-backlog | Execute a GitHub backlog plan by creating, updating, linking, closing, and commenting on issues from a handoff file | -| github-sprint-plan | Plan a GitHub milestone sprint by analyzing issue coverage, identifying gaps, and organizing work into a prioritized sprint backlog | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| github-triage-issues | Triage GitHub issues not yet triaged with automated label suggestions, milestone assignment, and duplicate detection | +| github-execute-backlog | Execute a GitHub backlog plan by creating, updating, linking, closing, and commenting on issues from a handoff file | +| github-sprint-plan | Plan a GitHub milestone sprint by analyzing issue coverage, identifying gaps, and organizing work into a prioritized sprint backlog | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | -| prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | -| github-backlog-discovery | Discovery protocol for GitHub backlog management - artifact-driven, user-centric, and search-based issue discovery | -| github-backlog-planning | Reference specification for GitHub backlog management tooling - planning files, search protocols, similarity assessment, and state persistence | -| github-backlog-triage | Triage workflow for GitHub issue backlog management - automated label suggestion, milestone assignment, and duplicate detection | -| github-backlog-update | Execution workflow for GitHub issue backlog management - consumes planning handoffs and executes issue operations | -| community-interaction | Community interaction voice, tone, and response templates for GitHub-facing agents and prompts | +| Instruction | Description | +| ----------- | ----------- | +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | +| github-backlog-discovery | Discovery protocol for GitHub backlog management - artifact-driven, user-centric, and search-based issue discovery | +| github-backlog-planning | Reference specification for GitHub backlog management tooling - planning files, search protocols, similarity assessment, and state persistence | +| github-backlog-triage | Triage workflow for GitHub issue backlog management - automated label suggestion, milestone assignment, and duplicate detection | +| github-backlog-update | Execution workflow for GitHub issue backlog management - consumes planning handoffs and executes issue operations | +| community-interaction | Community interaction voice, tone, and response templates for GitHub-facing agents and prompts | --- diff --git a/plugins/github/docs/templates b/plugins/github/docs/templates new file mode 120000 index 00000000..3c16d73f --- /dev/null +++ b/plugins/github/docs/templates @@ -0,0 +1 @@ +../../../docs/templates \ No newline at end of file diff --git a/plugins/github/scripts/dev-tools b/plugins/github/scripts/dev-tools new file mode 120000 index 00000000..00d24071 --- /dev/null +++ b/plugins/github/scripts/dev-tools @@ -0,0 +1 @@ +../../../scripts/dev-tools \ No newline at end of file diff --git a/plugins/github/scripts/lib b/plugins/github/scripts/lib new file mode 120000 index 00000000..4d903196 --- /dev/null +++ b/plugins/github/scripts/lib @@ -0,0 +1 @@ +../../../scripts/lib \ No newline at end of file diff --git a/plugins/hve-core-all/README.md b/plugins/hve-core-all/README.md index 56ab1cf3..084ab195 100644 --- a/plugins/hve-core-all/README.md +++ b/plugins/hve-core-all/README.md @@ -11,96 +11,94 @@ copilot plugin install hve-core-all@hve-core ## Agents -| Agent | Description | -|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ado-prd-to-wit | Product Manager expert for analyzing PRDs and planning Azure DevOps work item hierarchies | -| adr-creation | Interactive AI coaching for collaborative architectural decision record creation with guided discovery, research integration, and progressive documentation building - Brought to you by microsoft/edge-ai | -| arch-diagram-builder | Architecture diagram builder agent that builds high quality ASCII-art diagrams - Brought to you by microsoft/hve-core | -| brd-builder | Business Requirements Document builder with guided Q&A and reference integration | -| doc-ops | Autonomous documentation operations agent for pattern compliance, accuracy verification, and gap detection - Brought to you by microsoft/hve-core | -| gen-data-spec | Generate comprehensive data dictionaries, machine-readable data profiles, and objective summaries for downstream analysis (EDA notebooks, dashboards) through guided discovery | -| gen-jupyter-notebook | Create structured exploratory data analysis Jupyter notebooks from available data sources and generated data dictionaries | -| gen-streamlit-dashboard | Develop a multi-page Streamlit dashboard | -| github-backlog-manager | Orchestrator agent for GitHub backlog management workflows including triage, discovery, sprint planning, and execution - Brought to you by microsoft/hve-core | -| github-issue-manager | Deprecated: replaced by github-backlog-manager.agent.md for GitHub issue and backlog management | -| hve-core-installer | Decision-driven installer for HVE-Core with 6 installation methods for local, devcontainer, and Codespaces environments - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| pr-review | Comprehensive Pull Request review assistant ensuring code quality, security, and convention compliance - Brought to you by microsoft/hve-core | -| prd-builder | Product Requirements Document builder with guided Q&A and reference integration | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| security-plan-creator | Expert security architect for creating comprehensive cloud security plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| test-streamlit-dashboard | Automated testing for Streamlit dashboards using Playwright with issue tracking and reporting | +| Agent | Description | +| ----- | ----------- | +| ado-prd-to-wit | Product Manager expert for analyzing PRDs and planning Azure DevOps work item hierarchies | +| adr-creation | Interactive AI coaching for collaborative architectural decision record creation with guided discovery, research integration, and progressive documentation building - Brought to you by microsoft/edge-ai | +| arch-diagram-builder | Architecture diagram builder agent that builds high quality ASCII-art diagrams - Brought to you by microsoft/hve-core | +| brd-builder | Business Requirements Document builder with guided Q&A and reference integration | +| doc-ops | Autonomous documentation operations agent for pattern compliance, accuracy verification, and gap detection - Brought to you by microsoft/hve-core | +| gen-data-spec | Generate comprehensive data dictionaries, machine-readable data profiles, and objective summaries for downstream analysis (EDA notebooks, dashboards) through guided discovery | +| gen-jupyter-notebook | Create structured exploratory data analysis Jupyter notebooks from available data sources and generated data dictionaries | +| gen-streamlit-dashboard | Develop a multi-page Streamlit dashboard | +| github-backlog-manager | Orchestrator agent for GitHub backlog management workflows including triage, discovery, sprint planning, and execution - Brought to you by microsoft/hve-core | +| hve-core-installer | Decision-driven installer for HVE-Core with 6 installation methods for local, devcontainer, and Codespaces environments - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| pr-review | Comprehensive Pull Request review assistant ensuring code quality, security, and convention compliance - Brought to you by microsoft/hve-core | +| prd-builder | Product Requirements Document builder with guided Q&A and reference integration | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| security-plan-creator | Expert security architect for creating comprehensive cloud security plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| test-streamlit-dashboard | Automated testing for Streamlit dashboards using Playwright with issue tracking and reporting | ## Commands -| Command | Description | -|---------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| -| ado-create-pull-request | Generate pull request description, discover related work items, identify reviewers, and create Azure DevOps pull request with all linkages. | -| ado-get-build-info | Retrieve Azure DevOps build information for a Pull Request or specific Build Number. | -| ado-get-my-work-items | Retrieve user's current Azure DevOps work items and organize them into planning file definitions | -| ado-process-my-work-items-for-task-planning | Process retrieved work items for task planning and generate task-planning-logs.md handoff file | -| ado-update-wit-items | Prompt to update work items based on planning files | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| doc-ops-update | Invoke doc-ops agent for documentation quality assurance and updates | -| git-commit-message | Generates a commit message following the commit-message.instructions.md rules based on all changes in the branch | -| git-commit | Stages all changes, generates a conventional commit message, shows it to the user, and commits using only git add/commit | -| git-merge | Coordinate Git merge, rebase, and rebase --onto workflows with consistent conflict handling. | -| git-setup | Interactive, verification-first Git configuration assistant (non-destructive) | -| github-add-issue | Create a GitHub issue using discovered repository templates and conversational field collection | -| github-discover-issues | Discover GitHub issues through user-centric queries, artifact-driven analysis, or search-based exploration and produce planning files for review | -| github-execute-backlog | Execute a GitHub backlog plan by creating, updating, linking, closing, and commenting on issues from a handoff file | -| github-sprint-plan | Plan a GitHub milestone sprint by analyzing issue coverage, identifying gaps, and organizing work into a prioritized sprint backlog | -| github-triage-issues | Triage GitHub issues not yet triaged with automated label suggestions, milestone assignment, and duplicate detection | -| incident-response | Incident response workflow for Azure operations scenarios - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | -| pull-request | Provides prompt instructions for pull request (PR) generation - Brought to you by microsoft/edge-ai | -| risk-register | Creates a concise and well-structured qualitative risk register using a Probability ร— Impact (Pร—I) risk matrix. | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| Command | Description | +| ------- | ----------- | +| ado-create-pull-request | Generate pull request description, discover related work items, identify reviewers, and create Azure DevOps pull request with all linkages. | +| ado-get-build-info | Retrieve Azure DevOps build information for a Pull Request or specific Build Number. | +| ado-get-my-work-items | Retrieve user's current Azure DevOps work items and organize them into planning file definitions | +| ado-process-my-work-items-for-task-planning | Process retrieved work items for task planning and generate task-planning-logs.md handoff file | +| ado-update-wit-items | Prompt to update work items based on planning files | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| doc-ops-update | Invoke doc-ops agent for documentation quality assurance and updates | +| git-commit-message | Generates a commit message following the commit-message.instructions.md rules based on all changes in the branch | +| git-commit | Stages all changes, generates a conventional commit message, shows it to the user, and commits using only git add/commit | +| git-merge | Coordinate Git merge, rebase, and rebase --onto workflows with consistent conflict handling. | +| git-setup | Interactive, verification-first Git configuration assistant (non-destructive) | +| github-add-issue | Create a GitHub issue using discovered repository templates and conversational field collection | +| github-discover-issues | Discover GitHub issues through user-centric queries, artifact-driven analysis, or search-based exploration and produce planning files for review | +| github-execute-backlog | Execute a GitHub backlog plan by creating, updating, linking, closing, and commenting on issues from a handoff file | +| github-sprint-plan | Plan a GitHub milestone sprint by analyzing issue coverage, identifying gaps, and organizing work into a prioritized sprint backlog | +| github-triage-issues | Triage GitHub issues not yet triaged with automated label suggestions, milestone assignment, and duplicate detection | +| incident-response | Incident response workflow for Azure operations scenarios - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| pull-request | Provides prompt instructions for pull request (PR) generation - Brought to you by microsoft/edge-ai | +| risk-register | Creates a concise and well-structured qualitative risk register using a Probability ร— Impact (Pร—I) risk matrix. | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ado-create-pull-request | Required protocol for creating Azure DevOps pull requests with work item discovery, reviewer identification, and automated linking. | -| ado-get-build-info | Required instructions for anything related to Azure Devops or ado build information including status, logs, or details from provided pullrequest (PR), build Id, or branch name. | -| ado-update-wit-items | Work item creation and update protocol using MCP ADO tools with handoff tracking | -| ado-wit-discovery | Protocol for discovering Azure DevOps work items via user assignment or artifact analysis with planning file output | -| ado-wit-planning | Reference specification for Azure DevOps work item planning files, templates, field definitions, and search protocols | -| bash | Instructions for bash script implementation - Brought to you by microsoft/edge-ai | -| bicep | Instructions for Bicep infrastructure as code implementation - Brought to you by microsoft/hve-core | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | -| community-interaction | Community interaction voice, tone, and response templates for GitHub-facing agents and prompts | -| csharp-tests | Required instructions for C# (CSharp) test code research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | -| csharp | Required instructions for C# (CSharp) research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | -| git-merge | Required protocol for Git merge, rebase, and rebase --onto workflows with conflict handling and stop controls. | -| github-backlog-discovery | Discovery protocol for GitHub backlog management - artifact-driven, user-centric, and search-based issue discovery | -| github-backlog-planning | Reference specification for GitHub backlog management tooling - planning files, search protocols, similarity assessment, and state persistence | -| github-backlog-triage | Triage workflow for GitHub issue backlog management - automated label suggestion, milestone assignment, and duplicate detection | -| github-backlog-update | Execution workflow for GitHub issue backlog management - consumes planning handoffs and executes issue operations | -| hve-core-location | Important: hve-core is the repository containing this instruction file; Guidance: if a referenced prompt, instructions, agent, or script is missing in the current directory, fall back to this hve-core location by walking up this file's directory tree. | -| workflows | Required instructions for GitHub Actions workflow files in hve-core repository | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | -| python-script | Instructions for Python scripting implementation - Brought to you by microsoft/hve-core | -| terraform | Instructions for Terraform infrastructure as code implementation - Brought to you by microsoft/hve-core | -| uv-projects | Create and manage Python virtual environments using uv commands | -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| Instruction | Description | +| ----------- | ----------- | +| ado-create-pull-request | Required protocol for creating Azure DevOps pull requests with work item discovery, reviewer identification, and automated linking. | +| ado-get-build-info | Required instructions for anything related to Azure Devops or ado build information including status, logs, or details from provided pullrequest (PR), build Id, or branch name. | +| ado-update-wit-items | Work item creation and update protocol using MCP ADO tools with handoff tracking | +| ado-wit-discovery | Protocol for discovering Azure DevOps work items via user assignment or artifact analysis with planning file output | +| ado-wit-planning | Reference specification for Azure DevOps work item planning files, templates, field definitions, and search protocols | +| bash | Instructions for bash script implementation - Brought to you by microsoft/edge-ai | +| bicep | Instructions for Bicep infrastructure as code implementation - Brought to you by microsoft/hve-core | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| community-interaction | Community interaction voice, tone, and response templates for GitHub-facing agents and prompts | +| csharp-tests | Required instructions for C# (CSharp) test code research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | +| csharp | Required instructions for C# (CSharp) research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | +| git-merge | Required protocol for Git merge, rebase, and rebase --onto workflows with conflict handling and stop controls. | +| github-backlog-discovery | Discovery protocol for GitHub backlog management - artifact-driven, user-centric, and search-based issue discovery | +| github-backlog-planning | Reference specification for GitHub backlog management tooling - planning files, search protocols, similarity assessment, and state persistence | +| github-backlog-triage | Triage workflow for GitHub issue backlog management - automated label suggestion, milestone assignment, and duplicate detection | +| github-backlog-update | Execution workflow for GitHub issue backlog management - consumes planning handoffs and executes issue operations | +| hve-core-location | Important: hve-core is the repository containing this instruction file; Guidance: if a referenced prompt, instructions, agent, or script is missing in the current directory, fall back to this hve-core location by walking up this file's directory tree. | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | +| python-script | Instructions for Python scripting implementation - Brought to you by microsoft/hve-core | +| terraform | Instructions for Terraform infrastructure as code implementation - Brought to you by microsoft/hve-core | +| uv-projects | Create and manage Python virtual environments using uv commands | +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | ## Skills -| Skill | Description | -|--------------|--------------| +| Skill | Description | +| ----- | ----------- | | video-to-gif | video-to-gif | --- diff --git a/plugins/hve-core-all/agents/github-issue-manager.md b/plugins/hve-core-all/agents/github-issue-manager.md deleted file mode 120000 index 9d876439..00000000 --- a/plugins/hve-core-all/agents/github-issue-manager.md +++ /dev/null @@ -1 +0,0 @@ -../../../.github/agents/github-issue-manager.agent.md \ No newline at end of file diff --git a/plugins/hve-core-all/docs/templates b/plugins/hve-core-all/docs/templates new file mode 120000 index 00000000..3c16d73f --- /dev/null +++ b/plugins/hve-core-all/docs/templates @@ -0,0 +1 @@ +../../../docs/templates \ No newline at end of file diff --git a/plugins/hve-core-all/instructions/workflows.md b/plugins/hve-core-all/instructions/workflows.md deleted file mode 120000 index e497d5b9..00000000 --- a/plugins/hve-core-all/instructions/workflows.md +++ /dev/null @@ -1 +0,0 @@ -../../../.github/instructions/hve-core/workflows.instructions.md \ No newline at end of file diff --git a/plugins/hve-core-all/scripts/dev-tools b/plugins/hve-core-all/scripts/dev-tools new file mode 120000 index 00000000..00d24071 --- /dev/null +++ b/plugins/hve-core-all/scripts/dev-tools @@ -0,0 +1 @@ +../../../scripts/dev-tools \ No newline at end of file diff --git a/plugins/hve-core-all/scripts/lib b/plugins/hve-core-all/scripts/lib new file mode 120000 index 00000000..4d903196 --- /dev/null +++ b/plugins/hve-core-all/scripts/lib @@ -0,0 +1 @@ +../../../scripts/lib \ No newline at end of file diff --git a/plugins/project-planning/README.md b/plugins/project-planning/README.md index c7bf5da8..7c5307d8 100644 --- a/plugins/project-planning/README.md +++ b/plugins/project-planning/README.md @@ -11,43 +11,43 @@ copilot plugin install project-planning@hve-core ## Agents -| Agent | Description | -|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| doc-ops | Autonomous documentation operations agent for pattern compliance, accuracy verification, and gap detection - Brought to you by microsoft/hve-core | -| adr-creation | Interactive AI coaching for collaborative architectural decision record creation with guided discovery, research integration, and progressive documentation building - Brought to you by microsoft/edge-ai | -| arch-diagram-builder | Architecture diagram builder agent that builds high quality ASCII-art diagrams - Brought to you by microsoft/hve-core | -| brd-builder | Business Requirements Document builder with guided Q&A and reference integration | -| prd-builder | Product Requirements Document builder with guided Q&A and reference integration | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +| ----- | ----------- | +| doc-ops | Autonomous documentation operations agent for pattern compliance, accuracy verification, and gap detection - Brought to you by microsoft/hve-core | +| adr-creation | Interactive AI coaching for collaborative architectural decision record creation with guided discovery, research integration, and progressive documentation building - Brought to you by microsoft/edge-ai | +| arch-diagram-builder | Architecture diagram builder agent that builds high quality ASCII-art diagrams - Brought to you by microsoft/hve-core | +| brd-builder | Business Requirements Document builder with guided Q&A and reference integration | +| prd-builder | Product Requirements Document builder with guided Q&A and reference integration | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -|-----------------|------------------------------------------------------------------------------------------------------------------------------| -| doc-ops-update | Invoke doc-ops agent for documentation quality assurance and updates | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +| ------- | ----------- | +| doc-ops-update | Invoke doc-ops agent for documentation quality assurance and updates | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| Instruction | Description | +| ----------- | ----------- | +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | --- diff --git a/plugins/project-planning/docs/templates b/plugins/project-planning/docs/templates new file mode 120000 index 00000000..3c16d73f --- /dev/null +++ b/plugins/project-planning/docs/templates @@ -0,0 +1 @@ +../../../docs/templates \ No newline at end of file diff --git a/plugins/project-planning/scripts/dev-tools b/plugins/project-planning/scripts/dev-tools new file mode 120000 index 00000000..00d24071 --- /dev/null +++ b/plugins/project-planning/scripts/dev-tools @@ -0,0 +1 @@ +../../../scripts/dev-tools \ No newline at end of file diff --git a/plugins/project-planning/scripts/lib b/plugins/project-planning/scripts/lib new file mode 120000 index 00000000..4d903196 --- /dev/null +++ b/plugins/project-planning/scripts/lib @@ -0,0 +1 @@ +../../../scripts/lib \ No newline at end of file diff --git a/plugins/prompt-engineering/README.md b/plugins/prompt-engineering/README.md index 43220aa9..c06c89e9 100644 --- a/plugins/prompt-engineering/README.md +++ b/plugins/prompt-engineering/README.md @@ -11,37 +11,37 @@ copilot plugin install prompt-engineering@hve-core ## Agents -| Agent | Description | -|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +| ----- | ----------- | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -|-----------------|------------------------------------------------------------------------------------------------------------------------------| -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +| ------- | ----------- | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| Instruction | Description | +| ----------- | ----------- | +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | --- diff --git a/plugins/prompt-engineering/docs/templates b/plugins/prompt-engineering/docs/templates new file mode 120000 index 00000000..3c16d73f --- /dev/null +++ b/plugins/prompt-engineering/docs/templates @@ -0,0 +1 @@ +../../../docs/templates \ No newline at end of file diff --git a/plugins/prompt-engineering/scripts/dev-tools b/plugins/prompt-engineering/scripts/dev-tools new file mode 120000 index 00000000..00d24071 --- /dev/null +++ b/plugins/prompt-engineering/scripts/dev-tools @@ -0,0 +1 @@ +../../../scripts/dev-tools \ No newline at end of file diff --git a/plugins/prompt-engineering/scripts/lib b/plugins/prompt-engineering/scripts/lib new file mode 120000 index 00000000..4d903196 --- /dev/null +++ b/plugins/prompt-engineering/scripts/lib @@ -0,0 +1 @@ +../../../scripts/lib \ No newline at end of file diff --git a/plugins/rpi/README.md b/plugins/rpi/README.md index 7d3c5603..dec0c874 100644 --- a/plugins/rpi/README.md +++ b/plugins/rpi/README.md @@ -11,37 +11,37 @@ copilot plugin install rpi@hve-core ## Agents -| Agent | Description | -|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +| ----- | ----------- | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -|-----------------|------------------------------------------------------------------------------------------------------------------------------| -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +| ------- | ----------- | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| Instruction | Description | +| ----------- | ----------- | +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | --- diff --git a/plugins/rpi/docs/templates b/plugins/rpi/docs/templates new file mode 120000 index 00000000..3c16d73f --- /dev/null +++ b/plugins/rpi/docs/templates @@ -0,0 +1 @@ +../../../docs/templates \ No newline at end of file diff --git a/plugins/rpi/scripts/dev-tools b/plugins/rpi/scripts/dev-tools new file mode 120000 index 00000000..00d24071 --- /dev/null +++ b/plugins/rpi/scripts/dev-tools @@ -0,0 +1 @@ +../../../scripts/dev-tools \ No newline at end of file diff --git a/plugins/rpi/scripts/lib b/plugins/rpi/scripts/lib new file mode 120000 index 00000000..4d903196 --- /dev/null +++ b/plugins/rpi/scripts/lib @@ -0,0 +1 @@ +../../../scripts/lib \ No newline at end of file diff --git a/plugins/security-planning/README.md b/plugins/security-planning/README.md index 54ae4939..7ca2bef9 100644 --- a/plugins/security-planning/README.md +++ b/plugins/security-planning/README.md @@ -11,40 +11,40 @@ copilot plugin install security-planning@hve-core ## Agents -| Agent | Description | -|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| security-plan-creator | Expert security architect for creating comprehensive cloud security plans - Brought to you by microsoft/hve-core | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +| ----- | ----------- | +| security-plan-creator | Expert security architect for creating comprehensive cloud security plans - Brought to you by microsoft/hve-core | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -|-------------------|------------------------------------------------------------------------------------------------------------------------------| -| incident-response | Incident response workflow for Azure operations scenarios - Brought to you by microsoft/hve-core | -| risk-register | Creates a concise and well-structured qualitative risk register using a Probability ร— Impact (Pร—I) risk matrix. | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +| ------- | ----------- | +| incident-response | Incident response workflow for Azure operations scenarios - Brought to you by microsoft/hve-core | +| risk-register | Creates a concise and well-structured qualitative risk register using a Probability ร— Impact (Pร—I) risk matrix. | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| Instruction | Description | +| ----------- | ----------- | +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | --- diff --git a/plugins/security-planning/docs/templates b/plugins/security-planning/docs/templates new file mode 120000 index 00000000..3c16d73f --- /dev/null +++ b/plugins/security-planning/docs/templates @@ -0,0 +1 @@ +../../../docs/templates \ No newline at end of file diff --git a/plugins/security-planning/scripts/dev-tools b/plugins/security-planning/scripts/dev-tools new file mode 120000 index 00000000..00d24071 --- /dev/null +++ b/plugins/security-planning/scripts/dev-tools @@ -0,0 +1 @@ +../../../scripts/dev-tools \ No newline at end of file diff --git a/plugins/security-planning/scripts/lib b/plugins/security-planning/scripts/lib new file mode 120000 index 00000000..4d903196 --- /dev/null +++ b/plugins/security-planning/scripts/lib @@ -0,0 +1 @@ +../../../scripts/lib \ No newline at end of file diff --git a/scripts/plugins/Modules/PluginHelpers.psm1 b/scripts/plugins/Modules/PluginHelpers.psm1 index 0d388e45..320c016f 100644 --- a/scripts/plugins/Modules/PluginHelpers.psm1 +++ b/scripts/plugins/Modules/PluginHelpers.psm1 @@ -756,6 +756,30 @@ function Write-PluginDirectory { New-RelativeSymlink -SourcePath $sourcePath -DestinationPath $destPath } + # Symlink shared resource directories (unconditional, all plugins) + $sharedDirs = @( + @{ Source = 'docs/templates'; Destination = 'docs/templates' } + @{ Source = 'scripts/dev-tools'; Destination = 'scripts/dev-tools' } + @{ Source = 'scripts/lib'; Destination = 'scripts/lib' } + ) + + foreach ($dir in $sharedDirs) { + $sourcePath = Join-Path -Path $RepoRoot -ChildPath $dir.Source + $destPath = Join-Path -Path $pluginRoot -ChildPath $dir.Destination + + if (-not (Test-Path -Path $sourcePath)) { + Write-Warning "Shared directory not found: $sourcePath" + continue + } + + if ($DryRun) { + Write-Verbose "DryRun: Would create shared directory symlink $destPath -> $sourcePath" + continue + } + + New-RelativeSymlink -SourcePath $sourcePath -DestinationPath $destPath + } + # Generate plugin.json $manifestDir = Join-Path -Path $pluginRoot -ChildPath '.github' -AdditionalChildPath 'plugin' $manifestPath = Join-Path -Path $manifestDir -ChildPath 'plugin.json' From d84a8858f237c1591f7ed3f08c8ab5214097607c Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 22:50:23 -0800 Subject: [PATCH 52/62] chore(extension): remove outdated hve-core-2.2.0.zip file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ”’ - Generated by Copilot --- extension/hve-core-2.2.0.zip | Bin 300928 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 extension/hve-core-2.2.0.zip diff --git a/extension/hve-core-2.2.0.zip b/extension/hve-core-2.2.0.zip deleted file mode 100644 index 7069838e9225523c004c10686905b859f212b2cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 300928 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

=h;Ejw6H*EnpEVvPLY?94 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 F{{edX^TPlD From 285b074ab5e067d92748c754161764a98c5817ec Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 22:50:28 -0800 Subject: [PATCH 53/62] style(extension): update markdownlint configuration to ignore additional plugin files and collection markdowns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ”’ - Generated by Copilot --- .markdownlint-cli2.jsonc | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index ac3a4435..58407f28 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -5,6 +5,13 @@ ".copilot-tracking/**", "venv/**", "scripts/tests/Fixtures/**", - "extension/README.md" + "extension/README.md", + "collections/*.collection.md", + "plugins/**/agents/**", + "plugins/**/instructions/**", + "plugins/**/commands/**", + "plugins/**/skills/**", + "plugins/**/docs/**", + "plugins/**/scripts/**" ] } From a0c0080f908259a13c940189e9756b05426e46b4 Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 22:50:44 -0800 Subject: [PATCH 54/62] chore(agents): remove deprecated github-issue-manager agent from collection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ”’ - Generated by Copilot --- collections/hve-core-all.collection.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/collections/hve-core-all.collection.yml b/collections/hve-core-all.collection.yml index 1a12eb4f..d6e1ec7f 100644 --- a/collections/hve-core-all.collection.yml +++ b/collections/hve-core-all.collection.yml @@ -25,9 +25,6 @@ items: - path: .github/agents/github-backlog-manager.agent.md kind: agent maturity: experimental -- path: .github/agents/github-issue-manager.agent.md - kind: agent - maturity: deprecated - path: .github/agents/hve-core-installer.agent.md kind: agent - path: .github/agents/memory.agent.md From f745e18ac6f10a8199494c1dbd38626b78a38325 Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 23:02:48 -0800 Subject: [PATCH 55/62] feat(docs): update agent descriptions and commands in various README files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - standardize agent and command tables for clarity - add new github-issue-manager agent documentation - improve formatting for better readability - include additional instructions and skills ๐Ÿ”’ - Generated by Copilot --- collections/hve-core-all.collection.yml | 4 +- extension/PACKAGING.md | 16 +- plugins/ado/README.md | 74 ++++---- plugins/coding-standards/README.md | 64 +++---- plugins/data-science/README.md | 62 +++---- plugins/git/README.md | 64 +++---- plugins/github/README.md | 72 ++++---- plugins/hve-core-all/README.md | 159 +++++++++--------- .../agents/github-issue-manager.md | 1 + plugins/project-planning/README.md | 62 +++---- plugins/prompt-engineering/README.md | 50 +++--- plugins/rpi/README.md | 50 +++--- plugins/security-planning/README.md | 56 +++--- .../linting/Validate-MarkdownFrontmatter.ps1 | 2 + 14 files changed, 371 insertions(+), 365 deletions(-) create mode 120000 plugins/hve-core-all/agents/github-issue-manager.md diff --git a/collections/hve-core-all.collection.yml b/collections/hve-core-all.collection.yml index d6e1ec7f..136ecd19 100644 --- a/collections/hve-core-all.collection.yml +++ b/collections/hve-core-all.collection.yml @@ -25,6 +25,8 @@ items: - path: .github/agents/github-backlog-manager.agent.md kind: agent maturity: experimental +- path: .github/agents/github-issue-manager.agent.md + kind: agent - path: .github/agents/hve-core-installer.agent.md kind: agent - path: .github/agents/memory.agent.md @@ -162,5 +164,5 @@ items: - path: .github/skills/video-to-gif kind: skill display: - featured: true ordering: alpha + featured: true diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index b938a0fd..551ad8eb 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -357,19 +357,19 @@ The extension supports building persona-specific collection packages from a sing Collection manifests are defined in root `collections/` as YAML files: -| Collection | Manifest | Description | -|------------|------------------------------------|----------------------------------------| -| Full | `hve-core-all.collection.yml` | All artifacts regardless of persona | -| Developer | `developer.collection.yml` | Software engineering focused artifacts | +| Collection | Manifest | Description | +|------------|-------------------------------|----------------------------------------| +| Full | `hve-core-all.collection.yml` | All artifacts regardless of persona | +| Developer | `developer.collection.yml` | Software engineering focused artifacts | ### Persona Package Files All persona package files (`extension/package.json`, `extension/package.*.json`) are generated by `Prepare-Extension.ps1` from the source template and root collection YAML metadata. These files are gitignored build artifacts. -| Generated File | Source Collection | Purpose | -|--------------------------|---------------------------------|-----------------------------------| -| `package.json` | `hve-core-all.collection.yml` | Full bundle manifest | -| `package.{id}.json` | `{id}.collection.yml` | Persona edition metadata | +| Generated File | Source Collection | Purpose | +|---------------------|-------------------------------|--------------------------| +| `package.json` | `hve-core-all.collection.yml` | Full bundle manifest | +| `package.{id}.json` | `{id}.collection.yml` | Persona edition metadata | When building a persona collection, `Prepare-Extension.ps1`: diff --git a/plugins/ado/README.md b/plugins/ado/README.md index 7c81c9ec..00a5e172 100644 --- a/plugins/ado/README.md +++ b/plugins/ado/README.md @@ -11,49 +11,49 @@ copilot plugin install ado@hve-core ## Agents -| Agent | Description | -| ----- | ----------- | -| ado-prd-to-wit | Product Manager expert for analyzing PRDs and planning Azure DevOps work item hierarchies | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ado-prd-to-wit | Product Manager expert for analyzing PRDs and planning Azure DevOps work item hierarchies | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -| ------- | ----------- | -| ado-create-pull-request | Generate pull request description, discover related work items, identify reviewers, and create Azure DevOps pull request with all linkages. | -| ado-get-build-info | Retrieve Azure DevOps build information for a Pull Request or specific Build Number. | -| ado-get-my-work-items | Retrieve user's current Azure DevOps work items and organize them into planning file definitions | -| ado-process-my-work-items-for-task-planning | Process retrieved work items for task planning and generate task-planning-logs.md handoff file | -| ado-update-wit-items | Prompt to update work items based on planning files | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +|---------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------| +| ado-create-pull-request | Generate pull request description, discover related work items, identify reviewers, and create Azure DevOps pull request with all linkages. | +| ado-get-build-info | Retrieve Azure DevOps build information for a Pull Request or specific Build Number. | +| ado-get-my-work-items | Retrieve user's current Azure DevOps work items and organize them into planning file definitions | +| ado-process-my-work-items-for-task-planning | Process retrieved work items for task planning and generate task-planning-logs.md handoff file | +| ado-update-wit-items | Prompt to update work items based on planning files | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -| ----------- | ----------- | -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | -| prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | -| ado-create-pull-request | Required protocol for creating Azure DevOps pull requests with work item discovery, reviewer identification, and automated linking. | -| ado-get-build-info | Required instructions for anything related to Azure Devops or ado build information including status, logs, or details from provided pullrequest (PR), build Id, or branch name. | -| ado-update-wit-items | Work item creation and update protocol using MCP ADO tools with handoff tracking | -| ado-wit-discovery | Protocol for discovering Azure DevOps work items via user assignment or artifact analysis with planning file output | -| ado-wit-planning | Reference specification for Azure DevOps work item planning files, templates, field definitions, and search protocols | +| Instruction | Description | +|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | +| ado-create-pull-request | Required protocol for creating Azure DevOps pull requests with work item discovery, reviewer identification, and automated linking. | +| ado-get-build-info | Required instructions for anything related to Azure Devops or ado build information including status, logs, or details from provided pullrequest (PR), build Id, or branch name. | +| ado-update-wit-items | Work item creation and update protocol using MCP ADO tools with handoff tracking | +| ado-wit-discovery | Protocol for discovering Azure DevOps work items via user assignment or artifact analysis with planning file output | +| ado-wit-planning | Reference specification for Azure DevOps work item planning files, templates, field definitions, and search protocols | --- diff --git a/plugins/coding-standards/README.md b/plugins/coding-standards/README.md index e1713d37..b4738dcc 100644 --- a/plugins/coding-standards/README.md +++ b/plugins/coding-standards/README.md @@ -11,45 +11,45 @@ copilot plugin install coding-standards@hve-core ## Agents -| Agent | Description | -| ----- | ----------- | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -| ------- | ----------- | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +|-----------------|------------------------------------------------------------------------------------------------------------------------------| +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -| ----------- | ----------- | +| Instruction | Description | +|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | -| bash | Instructions for bash script implementation - Brought to you by microsoft/edge-ai | -| bicep | Instructions for Bicep infrastructure as code implementation - Brought to you by microsoft/hve-core | -| csharp | Required instructions for C# (CSharp) research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | -| csharp-tests | Required instructions for C# (CSharp) test code research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | -| python-script | Instructions for Python scripting implementation - Brought to you by microsoft/hve-core | -| terraform | Instructions for Terraform infrastructure as code implementation - Brought to you by microsoft/hve-core | -| uv-projects | Create and manage Python virtual environments using uv commands | -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| bash | Instructions for bash script implementation - Brought to you by microsoft/edge-ai | +| bicep | Instructions for Bicep infrastructure as code implementation - Brought to you by microsoft/hve-core | +| csharp | Required instructions for C# (CSharp) research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | +| csharp-tests | Required instructions for C# (CSharp) test code research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | +| python-script | Instructions for Python scripting implementation - Brought to you by microsoft/hve-core | +| terraform | Instructions for Terraform infrastructure as code implementation - Brought to you by microsoft/hve-core | +| uv-projects | Create and manage Python virtual environments using uv commands | +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | --- diff --git a/plugins/data-science/README.md b/plugins/data-science/README.md index e271e55d..ffbd7475 100644 --- a/plugins/data-science/README.md +++ b/plugins/data-science/README.md @@ -11,44 +11,44 @@ copilot plugin install data-science@hve-core ## Agents -| Agent | Description | -| ----- | ----------- | -| gen-data-spec | Generate comprehensive data dictionaries, machine-readable data profiles, and objective summaries for downstream analysis (EDA notebooks, dashboards) through guided discovery | -| gen-jupyter-notebook | Create structured exploratory data analysis Jupyter notebooks from available data sources and generated data dictionaries | -| gen-streamlit-dashboard | Develop a multi-page Streamlit dashboard | -| test-streamlit-dashboard | Automated testing for Streamlit dashboards using Playwright with issue tracking and reporting | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| gen-data-spec | Generate comprehensive data dictionaries, machine-readable data profiles, and objective summaries for downstream analysis (EDA notebooks, dashboards) through guided discovery | +| gen-jupyter-notebook | Create structured exploratory data analysis Jupyter notebooks from available data sources and generated data dictionaries | +| gen-streamlit-dashboard | Develop a multi-page Streamlit dashboard | +| test-streamlit-dashboard | Automated testing for Streamlit dashboards using Playwright with issue tracking and reporting | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -| ------- | ----------- | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +|-----------------|------------------------------------------------------------------------------------------------------------------------------| +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -| ----------- | ----------- | -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| Instruction | Description | +|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | -| python-script | Instructions for Python scripting implementation - Brought to you by microsoft/hve-core | -| uv-projects | Create and manage Python virtual environments using uv commands | +| python-script | Instructions for Python scripting implementation - Brought to you by microsoft/hve-core | +| uv-projects | Create and manage Python virtual environments using uv commands | --- diff --git a/plugins/git/README.md b/plugins/git/README.md index 633fd1bb..39067be9 100644 --- a/plugins/git/README.md +++ b/plugins/git/README.md @@ -11,45 +11,45 @@ copilot plugin install git@hve-core ## Agents -| Agent | Description | -| ----- | ----------- | -| pr-review | Comprehensive Pull Request review assistant ensuring code quality, security, and convention compliance - Brought to you by microsoft/hve-core | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| pr-review | Comprehensive Pull Request review assistant ensuring code quality, security, and convention compliance - Brought to you by microsoft/hve-core | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -| ------- | ----------- | -| git-commit-message | Generates a commit message following the commit-message.instructions.md rules based on all changes in the branch | -| git-commit | Stages all changes, generates a conventional commit message, shows it to the user, and commits using only git add/commit | -| git-merge | Coordinate Git merge, rebase, and rebase --onto workflows with consistent conflict handling. | -| git-setup | Interactive, verification-first Git configuration assistant (non-destructive) | -| pull-request | Provides prompt instructions for pull request (PR) generation - Brought to you by microsoft/edge-ai | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +|--------------------|------------------------------------------------------------------------------------------------------------------------------| +| git-commit-message | Generates a commit message following the commit-message.instructions.md rules based on all changes in the branch | +| git-commit | Stages all changes, generates a conventional commit message, shows it to the user, and commits using only git add/commit | +| git-merge | Coordinate Git merge, rebase, and rebase --onto workflows with consistent conflict handling. | +| git-setup | Interactive, verification-first Git configuration assistant (non-destructive) | +| pull-request | Provides prompt instructions for pull request (PR) generation - Brought to you by microsoft/edge-ai | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -| ----------- | ----------- | -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| Instruction | Description | +|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | -| git-merge | Required protocol for Git merge, rebase, and rebase --onto workflows with conflict handling and stop controls. | +| git-merge | Required protocol for Git merge, rebase, and rebase --onto workflows with conflict handling and stop controls. | --- diff --git a/plugins/github/README.md b/plugins/github/README.md index ed220b5b..10658693 100644 --- a/plugins/github/README.md +++ b/plugins/github/README.md @@ -11,49 +11,49 @@ copilot plugin install github@hve-core ## Agents -| Agent | Description | -| ----- | ----------- | -| github-backlog-manager | Orchestrator agent for GitHub backlog management workflows including triage, discovery, sprint planning, and execution - Brought to you by microsoft/hve-core | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +|------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| github-backlog-manager | Orchestrator agent for GitHub backlog management workflows including triage, discovery, sprint planning, and execution - Brought to you by microsoft/hve-core | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -| ------- | ----------- | -| github-add-issue | Create a GitHub issue using discovered repository templates and conversational field collection | +| Command | Description | +|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| +| github-add-issue | Create a GitHub issue using discovered repository templates and conversational field collection | | github-discover-issues | Discover GitHub issues through user-centric queries, artifact-driven analysis, or search-based exploration and produce planning files for review | -| github-triage-issues | Triage GitHub issues not yet triaged with automated label suggestions, milestone assignment, and duplicate detection | -| github-execute-backlog | Execute a GitHub backlog plan by creating, updating, linking, closing, and commenting on issues from a handoff file | -| github-sprint-plan | Plan a GitHub milestone sprint by analyzing issue coverage, identifying gaps, and organizing work into a prioritized sprint backlog | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| github-triage-issues | Triage GitHub issues not yet triaged with automated label suggestions, milestone assignment, and duplicate detection | +| github-execute-backlog | Execute a GitHub backlog plan by creating, updating, linking, closing, and commenting on issues from a handoff file | +| github-sprint-plan | Plan a GitHub milestone sprint by analyzing issue coverage, identifying gaps, and organizing work into a prioritized sprint backlog | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -| ----------- | ----------- | -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | -| prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | -| github-backlog-discovery | Discovery protocol for GitHub backlog management - artifact-driven, user-centric, and search-based issue discovery | -| github-backlog-planning | Reference specification for GitHub backlog management tooling - planning files, search protocols, similarity assessment, and state persistence | -| github-backlog-triage | Triage workflow for GitHub issue backlog management - automated label suggestion, milestone assignment, and duplicate detection | -| github-backlog-update | Execution workflow for GitHub issue backlog management - consumes planning handoffs and executes issue operations | -| community-interaction | Community interaction voice, tone, and response templates for GitHub-facing agents and prompts | +| Instruction | Description | +|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | +| github-backlog-discovery | Discovery protocol for GitHub backlog management - artifact-driven, user-centric, and search-based issue discovery | +| github-backlog-planning | Reference specification for GitHub backlog management tooling - planning files, search protocols, similarity assessment, and state persistence | +| github-backlog-triage | Triage workflow for GitHub issue backlog management - automated label suggestion, milestone assignment, and duplicate detection | +| github-backlog-update | Execution workflow for GitHub issue backlog management - consumes planning handoffs and executes issue operations | +| community-interaction | Community interaction voice, tone, and response templates for GitHub-facing agents and prompts | --- diff --git a/plugins/hve-core-all/README.md b/plugins/hve-core-all/README.md index 084ab195..5e7f8121 100644 --- a/plugins/hve-core-all/README.md +++ b/plugins/hve-core-all/README.md @@ -11,94 +11,95 @@ copilot plugin install hve-core-all@hve-core ## Agents -| Agent | Description | -| ----- | ----------- | -| ado-prd-to-wit | Product Manager expert for analyzing PRDs and planning Azure DevOps work item hierarchies | -| adr-creation | Interactive AI coaching for collaborative architectural decision record creation with guided discovery, research integration, and progressive documentation building - Brought to you by microsoft/edge-ai | -| arch-diagram-builder | Architecture diagram builder agent that builds high quality ASCII-art diagrams - Brought to you by microsoft/hve-core | -| brd-builder | Business Requirements Document builder with guided Q&A and reference integration | -| doc-ops | Autonomous documentation operations agent for pattern compliance, accuracy verification, and gap detection - Brought to you by microsoft/hve-core | -| gen-data-spec | Generate comprehensive data dictionaries, machine-readable data profiles, and objective summaries for downstream analysis (EDA notebooks, dashboards) through guided discovery | -| gen-jupyter-notebook | Create structured exploratory data analysis Jupyter notebooks from available data sources and generated data dictionaries | -| gen-streamlit-dashboard | Develop a multi-page Streamlit dashboard | -| github-backlog-manager | Orchestrator agent for GitHub backlog management workflows including triage, discovery, sprint planning, and execution - Brought to you by microsoft/hve-core | -| hve-core-installer | Decision-driven installer for HVE-Core with 6 installation methods for local, devcontainer, and Codespaces environments - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| pr-review | Comprehensive Pull Request review assistant ensuring code quality, security, and convention compliance - Brought to you by microsoft/hve-core | -| prd-builder | Product Requirements Document builder with guided Q&A and reference integration | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| security-plan-creator | Expert security architect for creating comprehensive cloud security plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| test-streamlit-dashboard | Automated testing for Streamlit dashboards using Playwright with issue tracking and reporting | +| Agent | Description | +|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ado-prd-to-wit | Product Manager expert for analyzing PRDs and planning Azure DevOps work item hierarchies | +| adr-creation | Interactive AI coaching for collaborative architectural decision record creation with guided discovery, research integration, and progressive documentation building - Brought to you by microsoft/edge-ai | +| arch-diagram-builder | Architecture diagram builder agent that builds high quality ASCII-art diagrams - Brought to you by microsoft/hve-core | +| brd-builder | Business Requirements Document builder with guided Q&A and reference integration | +| doc-ops | Autonomous documentation operations agent for pattern compliance, accuracy verification, and gap detection - Brought to you by microsoft/hve-core | +| gen-data-spec | Generate comprehensive data dictionaries, machine-readable data profiles, and objective summaries for downstream analysis (EDA notebooks, dashboards) through guided discovery | +| gen-jupyter-notebook | Create structured exploratory data analysis Jupyter notebooks from available data sources and generated data dictionaries | +| gen-streamlit-dashboard | Develop a multi-page Streamlit dashboard | +| github-backlog-manager | Orchestrator agent for GitHub backlog management workflows including triage, discovery, sprint planning, and execution - Brought to you by microsoft/hve-core | +| github-issue-manager | Deprecated: replaced by github-backlog-manager.agent.md for GitHub issue and backlog management | +| hve-core-installer | Decision-driven installer for HVE-Core with 6 installation methods for local, devcontainer, and Codespaces environments - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| pr-review | Comprehensive Pull Request review assistant ensuring code quality, security, and convention compliance - Brought to you by microsoft/hve-core | +| prd-builder | Product Requirements Document builder with guided Q&A and reference integration | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| security-plan-creator | Expert security architect for creating comprehensive cloud security plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| test-streamlit-dashboard | Automated testing for Streamlit dashboards using Playwright with issue tracking and reporting | ## Commands -| Command | Description | -| ------- | ----------- | -| ado-create-pull-request | Generate pull request description, discover related work items, identify reviewers, and create Azure DevOps pull request with all linkages. | -| ado-get-build-info | Retrieve Azure DevOps build information for a Pull Request or specific Build Number. | -| ado-get-my-work-items | Retrieve user's current Azure DevOps work items and organize them into planning file definitions | -| ado-process-my-work-items-for-task-planning | Process retrieved work items for task planning and generate task-planning-logs.md handoff file | -| ado-update-wit-items | Prompt to update work items based on planning files | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| doc-ops-update | Invoke doc-ops agent for documentation quality assurance and updates | -| git-commit-message | Generates a commit message following the commit-message.instructions.md rules based on all changes in the branch | -| git-commit | Stages all changes, generates a conventional commit message, shows it to the user, and commits using only git add/commit | -| git-merge | Coordinate Git merge, rebase, and rebase --onto workflows with consistent conflict handling. | -| git-setup | Interactive, verification-first Git configuration assistant (non-destructive) | -| github-add-issue | Create a GitHub issue using discovered repository templates and conversational field collection | -| github-discover-issues | Discover GitHub issues through user-centric queries, artifact-driven analysis, or search-based exploration and produce planning files for review | -| github-execute-backlog | Execute a GitHub backlog plan by creating, updating, linking, closing, and commenting on issues from a handoff file | -| github-sprint-plan | Plan a GitHub milestone sprint by analyzing issue coverage, identifying gaps, and organizing work into a prioritized sprint backlog | -| github-triage-issues | Triage GitHub issues not yet triaged with automated label suggestions, milestone assignment, and duplicate detection | -| incident-response | Incident response workflow for Azure operations scenarios - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | -| pull-request | Provides prompt instructions for pull request (PR) generation - Brought to you by microsoft/edge-ai | -| risk-register | Creates a concise and well-structured qualitative risk register using a Probability ร— Impact (Pร—I) risk matrix. | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| Command | Description | +|---------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| +| ado-create-pull-request | Generate pull request description, discover related work items, identify reviewers, and create Azure DevOps pull request with all linkages. | +| ado-get-build-info | Retrieve Azure DevOps build information for a Pull Request or specific Build Number. | +| ado-get-my-work-items | Retrieve user's current Azure DevOps work items and organize them into planning file definitions | +| ado-process-my-work-items-for-task-planning | Process retrieved work items for task planning and generate task-planning-logs.md handoff file | +| ado-update-wit-items | Prompt to update work items based on planning files | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| doc-ops-update | Invoke doc-ops agent for documentation quality assurance and updates | +| git-commit-message | Generates a commit message following the commit-message.instructions.md rules based on all changes in the branch | +| git-commit | Stages all changes, generates a conventional commit message, shows it to the user, and commits using only git add/commit | +| git-merge | Coordinate Git merge, rebase, and rebase --onto workflows with consistent conflict handling. | +| git-setup | Interactive, verification-first Git configuration assistant (non-destructive) | +| github-add-issue | Create a GitHub issue using discovered repository templates and conversational field collection | +| github-discover-issues | Discover GitHub issues through user-centric queries, artifact-driven analysis, or search-based exploration and produce planning files for review | +| github-execute-backlog | Execute a GitHub backlog plan by creating, updating, linking, closing, and commenting on issues from a handoff file | +| github-sprint-plan | Plan a GitHub milestone sprint by analyzing issue coverage, identifying gaps, and organizing work into a prioritized sprint backlog | +| github-triage-issues | Triage GitHub issues not yet triaged with automated label suggestions, milestone assignment, and duplicate detection | +| incident-response | Incident response workflow for Azure operations scenarios - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| pull-request | Provides prompt instructions for pull request (PR) generation - Brought to you by microsoft/edge-ai | +| risk-register | Creates a concise and well-structured qualitative risk register using a Probability ร— Impact (Pร—I) risk matrix. | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -| ----------- | ----------- | -| ado-create-pull-request | Required protocol for creating Azure DevOps pull requests with work item discovery, reviewer identification, and automated linking. | -| ado-get-build-info | Required instructions for anything related to Azure Devops or ado build information including status, logs, or details from provided pullrequest (PR), build Id, or branch name. | -| ado-update-wit-items | Work item creation and update protocol using MCP ADO tools with handoff tracking | -| ado-wit-discovery | Protocol for discovering Azure DevOps work items via user assignment or artifact analysis with planning file output | -| ado-wit-planning | Reference specification for Azure DevOps work item planning files, templates, field definitions, and search protocols | -| bash | Instructions for bash script implementation - Brought to you by microsoft/edge-ai | -| bicep | Instructions for Bicep infrastructure as code implementation - Brought to you by microsoft/hve-core | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | -| community-interaction | Community interaction voice, tone, and response templates for GitHub-facing agents and prompts | -| csharp-tests | Required instructions for C# (CSharp) test code research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | -| csharp | Required instructions for C# (CSharp) research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | -| git-merge | Required protocol for Git merge, rebase, and rebase --onto workflows with conflict handling and stop controls. | -| github-backlog-discovery | Discovery protocol for GitHub backlog management - artifact-driven, user-centric, and search-based issue discovery | -| github-backlog-planning | Reference specification for GitHub backlog management tooling - planning files, search protocols, similarity assessment, and state persistence | -| github-backlog-triage | Triage workflow for GitHub issue backlog management - automated label suggestion, milestone assignment, and duplicate detection | -| github-backlog-update | Execution workflow for GitHub issue backlog management - consumes planning handoffs and executes issue operations | -| hve-core-location | Important: hve-core is the repository containing this instruction file; Guidance: if a referenced prompt, instructions, agent, or script is missing in the current directory, fall back to this hve-core location by walking up this file's directory tree. | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | -| python-script | Instructions for Python scripting implementation - Brought to you by microsoft/hve-core | -| terraform | Instructions for Terraform infrastructure as code implementation - Brought to you by microsoft/hve-core | -| uv-projects | Create and manage Python virtual environments using uv commands | -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| Instruction | Description | +|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ado-create-pull-request | Required protocol for creating Azure DevOps pull requests with work item discovery, reviewer identification, and automated linking. | +| ado-get-build-info | Required instructions for anything related to Azure Devops or ado build information including status, logs, or details from provided pullrequest (PR), build Id, or branch name. | +| ado-update-wit-items | Work item creation and update protocol using MCP ADO tools with handoff tracking | +| ado-wit-discovery | Protocol for discovering Azure DevOps work items via user assignment or artifact analysis with planning file output | +| ado-wit-planning | Reference specification for Azure DevOps work item planning files, templates, field definitions, and search protocols | +| bash | Instructions for bash script implementation - Brought to you by microsoft/edge-ai | +| bicep | Instructions for Bicep infrastructure as code implementation - Brought to you by microsoft/hve-core | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| community-interaction | Community interaction voice, tone, and response templates for GitHub-facing agents and prompts | +| csharp-tests | Required instructions for C# (CSharp) test code research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | +| csharp | Required instructions for C# (CSharp) research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core | +| git-merge | Required protocol for Git merge, rebase, and rebase --onto workflows with conflict handling and stop controls. | +| github-backlog-discovery | Discovery protocol for GitHub backlog management - artifact-driven, user-centric, and search-based issue discovery | +| github-backlog-planning | Reference specification for GitHub backlog management tooling - planning files, search protocols, similarity assessment, and state persistence | +| github-backlog-triage | Triage workflow for GitHub issue backlog management - automated label suggestion, milestone assignment, and duplicate detection | +| github-backlog-update | Execution workflow for GitHub issue backlog management - consumes planning handoffs and executes issue operations | +| hve-core-location | Important: hve-core is the repository containing this instruction file; Guidance: if a referenced prompt, instructions, agent, or script is missing in the current directory, fall back to this hve-core location by walking up this file's directory tree. | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | +| python-script | Instructions for Python scripting implementation - Brought to you by microsoft/hve-core | +| terraform | Instructions for Terraform infrastructure as code implementation - Brought to you by microsoft/hve-core | +| uv-projects | Create and manage Python virtual environments using uv commands | +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | ## Skills -| Skill | Description | -| ----- | ----------- | +| Skill | Description | +|--------------|--------------| | video-to-gif | video-to-gif | --- diff --git a/plugins/hve-core-all/agents/github-issue-manager.md b/plugins/hve-core-all/agents/github-issue-manager.md new file mode 120000 index 00000000..9d876439 --- /dev/null +++ b/plugins/hve-core-all/agents/github-issue-manager.md @@ -0,0 +1 @@ +../../../.github/agents/github-issue-manager.agent.md \ No newline at end of file diff --git a/plugins/project-planning/README.md b/plugins/project-planning/README.md index 7c5307d8..c7bf5da8 100644 --- a/plugins/project-planning/README.md +++ b/plugins/project-planning/README.md @@ -11,43 +11,43 @@ copilot plugin install project-planning@hve-core ## Agents -| Agent | Description | -| ----- | ----------- | -| doc-ops | Autonomous documentation operations agent for pattern compliance, accuracy verification, and gap detection - Brought to you by microsoft/hve-core | -| adr-creation | Interactive AI coaching for collaborative architectural decision record creation with guided discovery, research integration, and progressive documentation building - Brought to you by microsoft/edge-ai | -| arch-diagram-builder | Architecture diagram builder agent that builds high quality ASCII-art diagrams - Brought to you by microsoft/hve-core | -| brd-builder | Business Requirements Document builder with guided Q&A and reference integration | -| prd-builder | Product Requirements Document builder with guided Q&A and reference integration | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| doc-ops | Autonomous documentation operations agent for pattern compliance, accuracy verification, and gap detection - Brought to you by microsoft/hve-core | +| adr-creation | Interactive AI coaching for collaborative architectural decision record creation with guided discovery, research integration, and progressive documentation building - Brought to you by microsoft/edge-ai | +| arch-diagram-builder | Architecture diagram builder agent that builds high quality ASCII-art diagrams - Brought to you by microsoft/hve-core | +| brd-builder | Business Requirements Document builder with guided Q&A and reference integration | +| prd-builder | Product Requirements Document builder with guided Q&A and reference integration | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -| ------- | ----------- | -| doc-ops-update | Invoke doc-ops agent for documentation quality assurance and updates | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +|-----------------|------------------------------------------------------------------------------------------------------------------------------| +| doc-ops-update | Invoke doc-ops agent for documentation quality assurance and updates | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -| ----------- | ----------- | -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| Instruction | Description | +|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | --- diff --git a/plugins/prompt-engineering/README.md b/plugins/prompt-engineering/README.md index c06c89e9..43220aa9 100644 --- a/plugins/prompt-engineering/README.md +++ b/plugins/prompt-engineering/README.md @@ -11,37 +11,37 @@ copilot plugin install prompt-engineering@hve-core ## Agents -| Agent | Description | -| ----- | ----------- | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -| ------- | ----------- | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +|-----------------|------------------------------------------------------------------------------------------------------------------------------| +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -| ----------- | ----------- | -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| Instruction | Description | +|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | --- diff --git a/plugins/rpi/README.md b/plugins/rpi/README.md index dec0c874..7d3c5603 100644 --- a/plugins/rpi/README.md +++ b/plugins/rpi/README.md @@ -11,37 +11,37 @@ copilot plugin install rpi@hve-core ## Agents -| Agent | Description | -| ----- | ----------- | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -| ------- | ----------- | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +|-----------------|------------------------------------------------------------------------------------------------------------------------------| +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -| ----------- | ----------- | -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| Instruction | Description | +|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | --- diff --git a/plugins/security-planning/README.md b/plugins/security-planning/README.md index 7ca2bef9..54ae4939 100644 --- a/plugins/security-planning/README.md +++ b/plugins/security-planning/README.md @@ -11,40 +11,40 @@ copilot plugin install security-planning@hve-core ## Agents -| Agent | Description | -| ----- | ----------- | -| security-plan-creator | Expert security architect for creating comprehensive cloud security plans - Brought to you by microsoft/hve-core | -| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | -| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | -| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | -| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | -| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | -| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | -| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | +| Agent | Description | +|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| security-plan-creator | Expert security architect for creating comprehensive cloud security plans - Brought to you by microsoft/hve-core | +| rpi-agent | Autonomous RPI orchestrator dispatching task-* agents through Research โ†’ Plan โ†’ Implement โ†’ Review โ†’ Discover phases - Brought to you by microsoft/hve-core | +| task-researcher | Task research specialist for comprehensive project analysis - Brought to you by microsoft/hve-core | +| task-planner | Implementation planner for creating actionable implementation plans - Brought to you by microsoft/hve-core | +| task-implementor | Executes implementation plans from .copilot-tracking/plans with progressive tracking and change records | +| task-reviewer | Reviews completed implementation work for accuracy, completeness, and convention compliance - Brought to you by microsoft/hve-core | +| memory | Conversation memory persistence for session continuity - Brought to you by microsoft/hve-core | +| prompt-builder | Prompt engineering assistant with phase-based workflow for creating and validating prompts, agents, and instructions files - Brought to you by microsoft/hve-core | ## Commands -| Command | Description | -| ------- | ----------- | -| incident-response | Incident response workflow for Azure operations scenarios - Brought to you by microsoft/hve-core | -| risk-register | Creates a concise and well-structured qualitative risk register using a Probability ร— Impact (Pร—I) risk matrix. | -| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | -| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | -| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | -| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | -| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | -| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | -| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | -| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | -| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | +| Command | Description | +|-------------------|------------------------------------------------------------------------------------------------------------------------------| +| incident-response | Incident response workflow for Azure operations scenarios - Brought to you by microsoft/hve-core | +| risk-register | Creates a concise and well-structured qualitative risk register using a Probability ร— Impact (Pร—I) risk matrix. | +| rpi | Autonomous Research-Plan-Implement-Review-Discover workflow for completing tasks - Brought to you by microsoft/hve-core | +| task-research | Initiates research for implementation planning based on user requirements - Brought to you by microsoft/hve-core | +| task-plan | Initiates implementation planning based on user context or research documents - Brought to you by microsoft/hve-core | +| task-implement | Locates and executes implementation plans using task-implementor mode - Brought to you by microsoft/hve-core | +| task-review | Initiates implementation review based on user context or automatic artifact discovery - Brought to you by microsoft/hve-core | +| checkpoint | Save or restore conversation context using memory files - Brought to you by microsoft/hve-core | +| prompt-analyze | Evaluates prompt engineering artifacts against quality criteria and reports findings - Brought to you by microsoft/hve-core | +| prompt-build | Build or improve prompt engineering artifacts following quality criteria - Brought to you by microsoft/hve-core | +| prompt-refactor | Refactors and cleans up prompt engineering artifacts through iterative improvement - Brought to you by microsoft/hve-core | ## Instructions -| Instruction | Description | -| ----------- | ----------- | -| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | -| markdown | Required instructions for creating or editing any Markdown (.md) files | -| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | +| Instruction | Description | +|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| writing-style | Required writing style conventions for voice, tone, and language in all markdown content | +| markdown | Required instructions for creating or editing any Markdown (.md) files | +| commit-message | Required instructions for creating all commit messages - Brought to you by microsoft/hve-core | | prompt-builder | Authoring standards for prompt engineering artifacts including file types, protocol patterns, writing style, and quality criteria - Brought to you by microsoft/hve-core | --- diff --git a/scripts/linting/Validate-MarkdownFrontmatter.ps1 b/scripts/linting/Validate-MarkdownFrontmatter.ps1 index 0d764214..76e86d6c 100644 --- a/scripts/linting/Validate-MarkdownFrontmatter.ps1 +++ b/scripts/linting/Validate-MarkdownFrontmatter.ps1 @@ -32,6 +32,8 @@ param( 'scripts/tests/Fixtures/**', 'extension/README.md', 'extension/README.*.md', + 'extension/templates/README.template.md', + 'collections/*.collection.md', 'pr.md', '.github/PULL_REQUEST_TEMPLATE.md', 'plugins/**' From 7ea1f09862a7afb5c383e5639e8685c6b0cf7f66 Mon Sep 17 00:00:00 2001 From: Allen Greaves Date: Thu, 12 Feb 2026 23:20:45 -0800 Subject: [PATCH 56/62] feat(plugin): add marketplace.json and update plugin versions to 2.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create marketplace.json for plugin metadata - update version for all plugins to 2.2.0 - enhance plugin manifest generation in scripts ๐Ÿ”ง - Generated by Copilot --- .github/plugin/marketplace.json | 73 ++++++++ plugins/ado/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../data-science/.github/plugin/plugin.json | 2 +- plugins/git/.github/plugin/plugin.json | 2 +- plugins/github/.github/plugin/plugin.json | 2 +- .../hve-core-all/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- plugins/rpi/.github/plugin/plugin.json | 2 +- .../.github/plugin/plugin.json | 2 +- release-please-config.json | 16 ++ scripts/plugins/Generate-Plugins.ps1 | 11 ++ scripts/plugins/Modules/PluginHelpers.psm1 | 164 +++++++++++++++++- 14 files changed, 269 insertions(+), 15 deletions(-) create mode 100644 .github/plugin/marketplace.json diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json new file mode 100644 index 00000000..fc9d7e32 --- /dev/null +++ b/.github/plugin/marketplace.json @@ -0,0 +1,73 @@ +{ + "name": "hve-core", + "metadata": { + "description": "HVE Core", + "version": "2.2.0", + "pluginRoot": "./plugins" + }, + "owner": { + "name": "Microsoft" + }, + "plugins": [ + { + "name": "ado", + "source": "./plugins/ado", + "description": "Azure DevOps work item management, build monitoring, and pull request creation", + "version": "2.2.0" + }, + { + "name": "coding-standards", + "source": "./plugins/coding-standards", + "description": "Language-specific coding instructions for bash, Bicep, C#, Python, and Terraform projects", + "version": "2.2.0" + }, + { + "name": "data-science", + "source": "./plugins/data-science", + "description": "Data specification generation, Jupyter notebooks, and Streamlit dashboards", + "version": "2.2.0" + }, + { + "name": "git", + "source": "./plugins/git", + "description": "Git commit messages, merges, setup, and pull request prompts", + "version": "2.2.0" + }, + { + "name": "github", + "source": "./plugins/github", + "description": "GitHub issue discovery, triage, sprint planning, and backlog execution agents and prompts", + "version": "2.2.0" + }, + { + "name": "hve-core-all", + "source": "./plugins/hve-core-all", + "description": "Full bundle of all stable HVE Core agents, prompts, instructions, and skills", + "version": "2.2.0" + }, + { + "name": "project-planning", + "source": "./plugins/project-planning", + "description": "PRDs, BRDs, ADRs, architecture diagrams, and documentation operations", + "version": "2.2.0" + }, + { + "name": "prompt-engineering", + "source": "./plugins/prompt-engineering", + "description": "Tools for analyzing, building, and refactoring prompts, agents, and instructions", + "version": "2.2.0" + }, + { + "name": "rpi", + "source": "./plugins/rpi", + "description": "Research, Plan, Implement, Review workflow agents and prompts for task-driven development", + "version": "2.2.0" + }, + { + "name": "security-planning", + "source": "./plugins/security-planning", + "description": "Security plan creation, incident response, and risk assessment", + "version": "2.2.0" + } + ] +} \ No newline at end of file diff --git a/plugins/ado/.github/plugin/plugin.json b/plugins/ado/.github/plugin/plugin.json index f9d4e025..f3dc6f3e 100644 --- a/plugins/ado/.github/plugin/plugin.json +++ b/plugins/ado/.github/plugin/plugin.json @@ -1,5 +1,5 @@ { "name": "ado", "description": "Azure DevOps work item management, build monitoring, and pull request creation", - "version": "1.0.0" + "version": "2.2.0" } \ No newline at end of file diff --git a/plugins/coding-standards/.github/plugin/plugin.json b/plugins/coding-standards/.github/plugin/plugin.json index 4ee53e25..3ec58ae3 100644 --- a/plugins/coding-standards/.github/plugin/plugin.json +++ b/plugins/coding-standards/.github/plugin/plugin.json @@ -1,5 +1,5 @@ { "name": "coding-standards", "description": "Language-specific coding instructions for bash, Bicep, C#, Python, and Terraform projects", - "version": "1.0.0" + "version": "2.2.0" } \ No newline at end of file diff --git a/plugins/data-science/.github/plugin/plugin.json b/plugins/data-science/.github/plugin/plugin.json index 965a6176..45ae59c2 100644 --- a/plugins/data-science/.github/plugin/plugin.json +++ b/plugins/data-science/.github/plugin/plugin.json @@ -1,5 +1,5 @@ { "name": "data-science", "description": "Data specification generation, Jupyter notebooks, and Streamlit dashboards", - "version": "1.0.0" + "version": "2.2.0" } \ No newline at end of file diff --git a/plugins/git/.github/plugin/plugin.json b/plugins/git/.github/plugin/plugin.json index d9ebfe91..60b68ca6 100644 --- a/plugins/git/.github/plugin/plugin.json +++ b/plugins/git/.github/plugin/plugin.json @@ -1,5 +1,5 @@ { "name": "git", "description": "Git commit messages, merges, setup, and pull request prompts", - "version": "1.0.0" + "version": "2.2.0" } \ No newline at end of file diff --git a/plugins/github/.github/plugin/plugin.json b/plugins/github/.github/plugin/plugin.json index 2eda2b9a..85bbece5 100644 --- a/plugins/github/.github/plugin/plugin.json +++ b/plugins/github/.github/plugin/plugin.json @@ -1,5 +1,5 @@ { "name": "github", "description": "GitHub issue discovery, triage, sprint planning, and backlog execution agents and prompts", - "version": "1.0.0" + "version": "2.2.0" } \ No newline at end of file diff --git a/plugins/hve-core-all/.github/plugin/plugin.json b/plugins/hve-core-all/.github/plugin/plugin.json index 6eade948..dbbba49d 100644 --- a/plugins/hve-core-all/.github/plugin/plugin.json +++ b/plugins/hve-core-all/.github/plugin/plugin.json @@ -1,5 +1,5 @@ { "name": "hve-core-all", "description": "Full bundle of all stable HVE Core agents, prompts, instructions, and skills", - "version": "1.0.0" + "version": "2.2.0" } \ No newline at end of file diff --git a/plugins/project-planning/.github/plugin/plugin.json b/plugins/project-planning/.github/plugin/plugin.json index 24a5c885..a2a65a0f 100644 --- a/plugins/project-planning/.github/plugin/plugin.json +++ b/plugins/project-planning/.github/plugin/plugin.json @@ -1,5 +1,5 @@ { "name": "project-planning", "description": "PRDs, BRDs, ADRs, architecture diagrams, and documentation operations", - "version": "1.0.0" + "version": "2.2.0" } \ No newline at end of file diff --git a/plugins/prompt-engineering/.github/plugin/plugin.json b/plugins/prompt-engineering/.github/plugin/plugin.json index 43d12055..c9215199 100644 --- a/plugins/prompt-engineering/.github/plugin/plugin.json +++ b/plugins/prompt-engineering/.github/plugin/plugin.json @@ -1,5 +1,5 @@ { "name": "prompt-engineering", "description": "Tools for analyzing, building, and refactoring prompts, agents, and instructions", - "version": "1.0.0" + "version": "2.2.0" } \ No newline at end of file diff --git a/plugins/rpi/.github/plugin/plugin.json b/plugins/rpi/.github/plugin/plugin.json index 2bfcde6d..1a3e2024 100644 --- a/plugins/rpi/.github/plugin/plugin.json +++ b/plugins/rpi/.github/plugin/plugin.json @@ -1,5 +1,5 @@ { "name": "rpi", "description": "Research, Plan, Implement, Review workflow agents and prompts for task-driven development", - "version": "1.0.0" + "version": "2.2.0" } \ No newline at end of file diff --git a/plugins/security-planning/.github/plugin/plugin.json b/plugins/security-planning/.github/plugin/plugin.json index 609afb78..c333d1ef 100644 --- a/plugins/security-planning/.github/plugin/plugin.json +++ b/plugins/security-planning/.github/plugin/plugin.json @@ -1,5 +1,5 @@ { "name": "security-planning", "description": "Security plan creation, incident response, and risk assessment", - "version": "1.0.0" + "version": "2.2.0" } \ No newline at end of file diff --git a/release-please-config.json b/release-please-config.json index feed0c30..e69a4927 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -23,6 +23,22 @@ "type": "json", "path": "extension/templates/package.template.json", "jsonpath": "$.version" + }, + { + "type": "json", + "path": ".github/plugin/marketplace.json", + "jsonpath": "$.metadata.version" + }, + { + "type": "json", + "path": ".github/plugin/marketplace.json", + "jsonpath": "$.plugins[*].version" + }, + { + "type": "json", + "path": "plugins/*/.github/plugin/plugin.json", + "jsonpath": "$.version", + "glob": true } ] } diff --git a/scripts/plugins/Generate-Plugins.ps1 b/scripts/plugins/Generate-Plugins.ps1 index 2730740c..b1331e49 100644 --- a/scripts/plugins/Generate-Plugins.ps1 +++ b/scripts/plugins/Generate-Plugins.ps1 @@ -196,6 +196,10 @@ function Invoke-PluginGeneration { $collectionsDir = Join-Path -Path $RepoRoot -ChildPath 'collections' $pluginsDir = Join-Path -Path $RepoRoot -ChildPath 'plugins' + # Read repo version from package.json for plugin manifests + $packageJsonPath = Join-Path -Path $RepoRoot -ChildPath 'package.json' + $repoVersion = (Get-Content -Path $packageJsonPath -Raw | ConvertFrom-Json).version + # Auto-update hve-core-all collection with discovered artifacts $updateResult = Update-HveCoreAllCollection -RepoRoot $RepoRoot -DryRun:$DryRun Write-Verbose "hve-core-all updated: $($updateResult.ItemCount) items ($($updateResult.AddedCount) added, $($updateResult.RemovedCount) removed)" @@ -253,6 +257,7 @@ function Invoke-PluginGeneration { $result = Write-PluginDirectory -Collection $filteredCollection ` -PluginsDir $pluginsDir ` -RepoRoot $RepoRoot ` + -Version $repoVersion ` -DryRun:$DryRun $itemCount = $filteredCollection.items.Count @@ -265,6 +270,12 @@ function Invoke-PluginGeneration { Write-Host " $id ($itemCount items)" -ForegroundColor Green } + # Generate marketplace.json from all collections + Write-MarketplaceManifest ` + -RepoRoot $RepoRoot ` + -Collections $allCollections ` + -DryRun:$DryRun + Write-Host "`n--- Summary ---" -ForegroundColor Cyan Write-Host " Plugins generated: $generated" Write-Host " Agents: $totalAgents" diff --git a/scripts/plugins/Modules/PluginHelpers.psm1 b/scripts/plugins/Modules/PluginHelpers.psm1 index 320c016f..494ffbc7 100644 --- a/scripts/plugins/Modules/PluginHelpers.psm1 +++ b/scripts/plugins/Modules/PluginHelpers.psm1 @@ -465,7 +465,7 @@ function New-PluginManifestContent { .DESCRIPTION Creates a hashtable representing the plugin manifest with name, - description, and a default version of 1.0.0. + description, and version sourced from the repository package.json. .PARAMETER CollectionId The collection identifier used as the plugin name. @@ -473,6 +473,9 @@ function New-PluginManifestContent { .PARAMETER Description A short description of the plugin. + .PARAMETER Version + Semantic version string from the repository package.json. + .OUTPUTS [hashtable] Plugin manifest with name, description, and version keys. #> @@ -483,13 +486,16 @@ function New-PluginManifestContent { [string]$CollectionId, [Parameter(Mandatory = $true)] - [string]$Description + [string]$Description, + + [Parameter(Mandatory = $true)] + [string]$Version ) return [ordered]@{ name = $CollectionId description = $Description - version = '1.0.0' + version = $Version } } @@ -570,6 +576,146 @@ function New-PluginReadmeContent { return $sb.ToString() } +function New-MarketplaceManifestContent { + <# + .SYNOPSIS + Generates marketplace.json content as a hashtable. + + .DESCRIPTION + Creates a hashtable representing the marketplace manifest with repository + metadata, owner information, and plugin entries. Matches the schema used + by github/awesome-copilot. + + .PARAMETER RepoName + Repository name used as the marketplace name. + + .PARAMETER Description + Short description of the repository. + + .PARAMETER Version + Semantic version string from package.json. + + .PARAMETER OwnerName + Organization or individual owning the repository. + + .PARAMETER Plugins + Array of ordered hashtables with name, description, and version keys + from New-PluginManifestContent. + + .OUTPUTS + [hashtable] Marketplace manifest with name, metadata, owner, and plugins keys. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string]$RepoName, + + [Parameter(Mandatory = $true)] + [string]$Description, + + [Parameter(Mandatory = $true)] + [string]$Version, + + [Parameter(Mandatory = $true)] + [string]$OwnerName, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [array]$Plugins + ) + + $pluginEntries = @() + foreach ($plugin in $Plugins) { + $pluginEntries += [ordered]@{ + name = $plugin.name + source = "./plugins/$($plugin.name)" + description = $plugin.description + version = $plugin.version + } + } + + return [ordered]@{ + name = $RepoName + metadata = [ordered]@{ + description = $Description + version = $Version + pluginRoot = './plugins' + } + owner = [ordered]@{ + name = $OwnerName + } + plugins = $pluginEntries + } +} + +function Write-MarketplaceManifest { + <# + .SYNOPSIS + Writes the marketplace.json file to .github/plugin/. + + .DESCRIPTION + Assembles plugin metadata from generated collections and writes the + marketplace manifest to .github/plugin/marketplace.json. Creates the + directory when it does not exist. + + .PARAMETER RepoRoot + Absolute path to the repository root directory. + + .PARAMETER Collections + Array of collection manifest hashtables with id and description. + + .PARAMETER DryRun + When specified, logs the action without writing to disk. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$RepoRoot, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [array]$Collections, + + [Parameter(Mandatory = $false)] + [switch]$DryRun + ) + + $packageJsonPath = Join-Path -Path $RepoRoot -ChildPath 'package.json' + $packageJson = Get-Content -Path $packageJsonPath -Raw | ConvertFrom-Json + + $plugins = @() + foreach ($collection in ($Collections | Sort-Object { $_.id })) { + $plugins += New-PluginManifestContent ` + -CollectionId $collection.id ` + -Description $collection.description ` + -Version $packageJson.version + } + + $manifest = New-MarketplaceManifestContent ` + -RepoName $packageJson.name ` + -Description $packageJson.description ` + -Version $packageJson.version ` + -OwnerName $packageJson.author ` + -Plugins $plugins + + $outputDir = Join-Path -Path $RepoRoot -ChildPath '.github' -AdditionalChildPath 'plugin' + $outputPath = Join-Path -Path $outputDir -ChildPath 'marketplace.json' + + if ($DryRun) { + Write-Host " [DRY RUN] Would write marketplace.json at $outputPath" -ForegroundColor Yellow + return + } + + if (-not (Test-Path -Path $outputDir)) { + New-Item -ItemType Directory -Path $outputDir -Force | Out-Null + } + + $manifest | ConvertTo-Json -Depth 10 | Set-Content -Path $outputPath -Encoding utf8 -NoNewline + Write-Host " Marketplace manifest: $outputPath" -ForegroundColor Green +} + function New-GenerateResult { <# .SYNOPSIS @@ -670,6 +816,9 @@ function Write-PluginDirectory { .PARAMETER RepoRoot Absolute path to the repository root. + .PARAMETER Version + Semantic version string from the repository package.json. + .PARAMETER DryRun When specified, logs actions without creating files or directories. @@ -689,6 +838,9 @@ function Write-PluginDirectory { [Parameter(Mandatory = $true)] [string]$RepoRoot, + [Parameter(Mandatory = $true)] + [string]$Version, + [Parameter(Mandatory = $false)] [switch]$DryRun ) @@ -783,7 +935,7 @@ function Write-PluginDirectory { # Generate plugin.json $manifestDir = Join-Path -Path $pluginRoot -ChildPath '.github' -AdditionalChildPath 'plugin' $manifestPath = Join-Path -Path $manifestDir -ChildPath 'plugin.json' - $manifest = New-PluginManifestContent -CollectionId $collectionId -Description $Collection.description + $manifest = New-PluginManifestContent -CollectionId $collectionId -Description $Collection.description -Version $Version if ($DryRun) { Write-Verbose "DryRun: Would write plugin.json at $manifestPath" @@ -816,18 +968,20 @@ function Write-PluginDirectory { } Export-ModuleMember -Function @( + 'Get-AllCollections', 'Get-ArtifactFiles', 'Get-ArtifactFrontmatter', - 'Get-AllCollections', 'Get-CollectionManifest', 'Get-PluginItemName', 'Get-PluginSubdirectory', 'New-GenerateResult', + 'New-MarketplaceManifestContent', 'New-PluginManifestContent', 'New-PluginReadmeContent', 'New-RelativeSymlink', 'Resolve-CollectionItemMaturity', 'Test-ArtifactDeprecated', 'Update-HveCoreAllCollection', + 'Write-MarketplaceManifest', 'Write-PluginDirectory' ) From 621af8cb2657bde6219c677c073408484ad4334c Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 13 Feb 2026 10:14:18 +0100 Subject: [PATCH 57/62] refactor(scripts): rename persona terminology to collection across codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rename Get-PersonaReadmePath/Set-PersonaReadme to Get-CollectionReadmePath/Set-CollectionReadme - update all docstrings, comments, test names, and test fixtures - delete orphaned collection.schema.json - update documentation, agent, and config files (~18 files total) - preserve behavioral persona references in installer agent โ™ป๏ธ - Generated by Copilot --- .github/agents/hve-core-installer.agent.md | 68 +++++++------- collections/hve-core-all.collection.md | 2 +- docs/architecture/ai-artifacts.md | 22 ++--- docs/architecture/workflows.md | 4 +- docs/contributing/ai-artifacts-common.md | 40 ++++----- docs/contributing/custom-agents.md | 6 +- docs/contributing/instructions.md | 22 ++--- docs/contributing/prompts.md | 24 ++--- docs/contributing/skills.md | 6 +- docs/getting-started/install.md | 10 +-- extension/.vscodeignore | 4 +- extension/PACKAGING.md | 64 ++++++------- scripts/extension/Package-Extension.ps1 | 54 +++++------ scripts/extension/Prepare-Extension.ps1 | 32 +++---- .../linting/schemas/collection.schema.json | 90 ------------------- .../extension/Package-Extension.Tests.ps1 | 58 ++++++------ .../extension/Prepare-Extension.Tests.ps1 | 54 +++++------ 17 files changed, 228 insertions(+), 332 deletions(-) delete mode 100644 scripts/linting/schemas/collection.schema.json diff --git a/.github/agents/hve-core-installer.agent.md b/.github/agents/hve-core-installer.agent.md index 497e61de..ebe91624 100644 --- a/.github/agents/hve-core-installer.agent.md +++ b/.github/agents/hve-core-installer.agent.md @@ -1065,7 +1065,7 @@ Copying agents enables local customization and offline use. Options: [1] Install RPI Core only (recommended) - [2] Install by persona collection + [2] Install by collection [3] Skip agent installation Your choice? (1/2/3) @@ -1075,56 +1075,56 @@ Your choice? (1/2/3) User input handling: * "1", "rpi", "rpi core", "core" โ†’ Copy RPI Core bundle only -* "2", "persona", "collection", "by persona" โ†’ Proceed to Persona Selection sub-flow +* "2", "collection", "by collection" โ†’ Proceed to Collection Selection sub-flow * "3", "skip", "none", "no" โ†’ Skip to success report * Unclear response โ†’ Ask for clarification -### Persona Selection Sub-Flow +### Collection Selection Sub-Flow -When the user selects option 2, read collection manifests to present available personas. +When the user selects option 2, read collection manifests to present available collections. -#### Step 1: Read collections and build persona agent counts +#### Step 1: Read collections and build collection agent counts -Read `collections/*.collection.yml` from the HVE-Core source (at `$hveCoreBasePath`). Derive persona options from collection `id` and `name`. For each selected collection, count agent items where `kind` equals `agent` and effective item maturity is `stable` (item `maturity` omitted defaults to `stable`; exclude `experimental` and `deprecated`). +Read `collections/*.collection.yml` from the HVE-Core source (at `$hveCoreBasePath`). Derive collection options from collection `id` and `name`. For each selected collection, count agent items where `kind` equals `agent` and effective item maturity is `stable` (item `maturity` omitted defaults to `stable`; exclude `experimental` and `deprecated`). -#### Step 2: Present persona options +#### Step 2: Present collection options - + ```text -๐ŸŽญ Persona Collection Selection +๐ŸŽญ Collection Selection -Choose one or more personas to install agents tailored to your role, more to come in the future. +Choose one or more collections to install agents tailored to your role, more to come in the future. -| # | Persona | Agents | Description | -|---|-----------|--------|---------------------------------| -| 1 | Developer | [N] | Software engineers writing code | +| # | Collection | Agents | Description | +|---|------------|--------|---------------------------------| +| 1 | Developer | [N] | Software engineers writing code | -Enter persona number(s) separated by commas (e.g., "1"): +Enter collection number(s) separated by commas (e.g., "1"): ``` - + -Agent counts `[N]` include agents matching the persona with `stable` maturity. +Agent counts `[N]` include agents matching the collection with `stable` maturity. User input handling: -* Single number (e.g., "1") โ†’ Select that persona -* Multiple numbers (e.g., "1, 3") โ†’ Combine agent sets from selected personas -* Persona name (e.g., "developer") โ†’ Match by identifier +* Single number (e.g., "1") โ†’ Select that collection +* Multiple numbers (e.g., "1, 3") โ†’ Combine agent sets from selected collections +* Collection name (e.g., "developer") โ†’ Match by identifier * Unclear response โ†’ Ask for clarification #### Step 3: Build filtered agent list -For each selected persona identifier: +For each selected collection identifier: 1. Iterate through `agents` in the registry -2. Include agents where `maturity` is `stable` AND `personas` array contains the selected persona identifier -3. Deduplicate across multiple selected personas +2. Include agents where `maturity` is `stable` AND the collection's items list contains the agent +3. Deduplicate across multiple selected collections #### Step 4: Present filtered agents for confirmation - + ```text -๐Ÿ“‹ Agents for [Persona Name(s)] +๐Ÿ“‹ Agents for [Collection Name(s)] The following [N] agents will be copied: @@ -1134,7 +1134,7 @@ The following [N] agents will be copied: Proceed with installation? (yes/no) ``` - + User input handling: @@ -1143,20 +1143,20 @@ User input handling: * Unclear response โ†’ Ask for clarification > [!NOTE] -> Persona filtering applies to agents only. Copying of related prompts, instructions, and skills based on persona is planned for a future release. +> Collection filtering applies to agents only. Copying of related prompts, instructions, and skills based on collection is planned for a future release. ### Agent Bundle Definitions -| Bundle | Agents | -|----------------|---------------------------------------------------------------------------| -| `rpi-core` | task-researcher, task-planner, task-implementor, task-reviewer, rpi-agent | -| `persona:` | Stable agents matching the persona | +| Bundle | Agents | +|-------------------|---------------------------------------------------------------------------| +| `rpi-core` | task-researcher, task-planner, task-implementor, task-reviewer, rpi-agent | +| `collection:` | Stable agents matching the collection | ### Collision Detection Before copying, check for existing agent files with matching names. Generate a script for the user's shell that: -1. Builds list of source files based on selection (`rpi-core` = 5 files, `persona` = filtered `.agent.md` files) +1. Builds list of source files based on selection (`rpi-core` = 5 files, `collection` = filtered `.agent.md` files) 2. Copies files with `.agent.md` extension 3. Checks target directory (`.github/agents/`) for each name 4. Reports collisions or clean state @@ -1172,8 +1172,8 @@ $targetDir = ".github/agents" $filesToCopy = switch ($selection) { "rpi-core" { @("task-researcher.agent.md", "task-planner.agent.md", "task-implementor.agent.md", "task-reviewer.agent.md", "rpi-agent.agent.md") } default { - # Persona-based: $selection contains filtered agent names from registry - $personaAgents + # Collection-based: $selection contains filtered agent names from registry + $collectionAgents } } @@ -1256,7 +1256,7 @@ $manifest = @{ source = "microsoft/hve-core" version = (Get-Content "$hveCoreBasePath/package.json" | ConvertFrom-Json).version installed = (Get-Date -Format "o") - collection = $collectionId # "rpi-core" or persona id(s) e.g. "developer" or "developer,devops" + collection = $collectionId # "rpi-core" or collection id(s) e.g. "developer" or "developer,devops" files = @{}; skip = @() } diff --git a/collections/hve-core-all.collection.md b/collections/hve-core-all.collection.md index cdb96315..17f69cfd 100644 --- a/collections/hve-core-all.collection.md +++ b/collections/hve-core-all.collection.md @@ -1,3 +1,3 @@ HVE Core provides the complete collection of AI chat agents, prompts, instructions, and skills for VS Code with GitHub Copilot. This edition includes every artifact across all domains โ€” development workflows, architecture, Azure DevOps, data science, security, and more. -Use this edition when you want access to everything without choosing a focused persona. +Use this edition when you want access to everything without choosing a focused collection. diff --git a/docs/architecture/ai-artifacts.md b/docs/architecture/ai-artifacts.md index b4fcb4b3..cb083f0b 100644 --- a/docs/architecture/ai-artifacts.md +++ b/docs/architecture/ai-artifacts.md @@ -83,7 +83,7 @@ Instructions answer the question "what standards apply to this context?" and ens Instructions placed in `.github/instructions/hve-core/` are scoped to the hve-core repository itself and MUST NOT be included in collection manifests. These files govern internal repository concerns (CI/CD workflows, repo-specific conventions) that are not applicable outside the repository. Collection manifests intentionally exclude this subdirectory from artifact selection and package composition. > [!IMPORTANT] -> The `.github/instructions/hve-core/` directory is reserved for repo-specific instructions. Files in this directory are never distributed through extension packages or persona collections. +> The `.github/instructions/hve-core/` directory is reserved for repo-specific instructions. Files in this directory are never distributed through extension packages or collections. ### Skills @@ -250,20 +250,20 @@ items: | `kind` | Artifact type (`agent`, `prompt`, `instruction`, `skill`, `hook`) | | `maturity` | Optional release channel gating value (`stable` default) | -### Persona Model +### Collection Model -Personas represent user roles that consume artifacts. Collection manifests select artifacts for those personas. +Collections represent role-targeted artifact packages. Collection manifests select artifacts for those roles. -| Persona | Identifier | Target Users | +| Collection | Identifier | Target Users | |---------------|----------------|---------------------| | **All** | `hve-core-all` | Universal inclusion | | **Developer** | `developer` | Software engineers | -Artifacts assigned to `hve-core-all` appear in the full collection and may also include role-specific personas for targeted distribution. +Artifacts assigned to `hve-core-all` appear in the full collection and may also include role-specific collections for targeted distribution. ### Collection Build System -Collections define persona-filtered artifact packages. Each collection manifest specifies which personas to include and controls release channel eligibility through a `maturity` field: +Collections define role-filtered artifact packages. Each collection manifest specifies which artifacts to include and controls release channel eligibility through a `maturity` field: ```json { @@ -272,17 +272,17 @@ Collections define persona-filtered artifact packages. Each collection manifest "displayName": "HVE Core - Developer Edition", "description": "AI-powered coding agents curated for software engineers", "maturity": "stable", - "personas": ["developer"] + "items": ["developer"] } ``` The build system resolves collections by: -1. Reading the collection manifest to identify target personas +1. Reading the collection manifest to identify target artifacts 2. Checking collection-level maturity against the target release channel 3. Filtering collection items by path/kind membership -4. Including the `hve-core-all` persona artifacts as the base -5. Adding persona-specific artifacts +4. Including the `hve-core-all` collection artifacts as the base +5. Adding collection-specific artifacts 6. Resolving dependencies for included artifacts #### Collection Maturity @@ -312,7 +312,7 @@ graph TD A --> G[rpi.prompt] ``` -When installing `rpi-agent`, all dependent agents and prompts are automatically included regardless of persona filter. +When installing `rpi-agent`, all dependent agents and prompts are automatically included regardless of collection filter. ## Extension Integration diff --git a/docs/architecture/workflows.md b/docs/architecture/workflows.md index cc366f0a..119211b4 100644 --- a/docs/architecture/workflows.md +++ b/docs/architecture/workflows.md @@ -177,7 +177,7 @@ The `weekly-security-maintenance.yml` workflow runs every Sunday at 2AM UTC, pro ## Extension Publishing -The `extension-publish.yml` and `extension-publish-prerelease.yml` workflows handle VS Code extension marketplace publishing through manual dispatch. Both workflows use collection-based packaging to produce and publish a separate VSIX per persona collection. +The `extension-publish.yml` and `extension-publish-prerelease.yml` workflows handle VS Code extension marketplace publishing through manual dispatch. Both workflows use collection-based packaging to produce and publish a separate VSIX per collection. ```mermaid flowchart LR @@ -200,7 +200,7 @@ flowchart LR ### Collection-Based Packaging -Collection manifests in `collections/*.collection.yml` define persona-scoped subsets of the full artifact set. The `extension-package.yml` reusable workflow discovers these manifests, filters by maturity and channel, and packages each as an independent VSIX. +Collection manifests in `collections/*.collection.yml` define collection-scoped subsets of the full artifact set. The `extension-package.yml` reusable workflow discovers these manifests, filters by maturity and channel, and packages each as an independent VSIX. | Collection | Maturity | Included In | |----------------|--------------|--------------------| diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 35f8d081..9b0666b8 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -145,57 +145,57 @@ Instructions placed in `.github/instructions/hve-core/` are repo-specific and MU * Collection manifests * Extension packaging and distribution -* Persona collection builds +* Collection builds * Artifact selection for published bundles If your instructions apply only to the hve-core repository and are not intended for distribution to consumers, place them in `.github/instructions/hve-core/`. Otherwise, place them in `.github/instructions/` or a technology-specific subdirectory (e.g., `csharp/`, `bash/`). -## Persona Taxonomy +## Collection Taxonomy -Personas represent user roles that consume HVE-Core artifacts. The persona system enables role-specific artifact collections without fragmenting the codebase. +Collections represent role-targeted artifact packages for HVE-Core artifacts. The collection system enables role-specific artifact distribution without fragmenting the codebase. -### Defined Personas +### Defined Collections -| Persona | Identifier | Description | +| Collection | Identifier | Description | |---------------|----------------|---------------------------------| | **All** | `hve-core-all` | Full release with all artifacts | | **Developer** | `developer` | Software engineers writing code | -### Persona Assignment Guidelines +### Collection Assignment Guidelines -When assigning personas to artifacts: +When assigning collections to artifacts: -* **Universal artifacts** should include `hve-core-all` plus any role-specific personas that particularly benefit -* **Role-specific artifacts** should include only the relevant personas (omit `hve-core-all` for highly specialized artifacts) -* **Cross-cutting tools** like RPI workflow artifacts (`task-researcher`, `task-planner`) should include multiple relevant personas +* **Universal artifacts** should include `hve-core-all` plus any role-specific collections that particularly benefit +* **Role-specific artifacts** should include only the relevant collections (omit `hve-core-all` for highly specialized artifacts) +* **Cross-cutting tools** like RPI workflow artifacts (`task-researcher`, `task-planner`) should include multiple relevant collections -**Example persona assignments:** +**Example collection assignments:** ```json // Universal - available in all collections "markdown": { - "personas": ["hve-core-all", "developer"] + "collections": ["hve-core-all", "developer"] } // Developer-focused - targeted distribution "csharp/csharp": { - "personas": ["hve-core-all", "developer"] + "collections": ["hve-core-all", "developer"] } // Core workflow - broadly applicable "rpi-agent": { - "personas": ["hve-core-all", "developer"] + "collections": ["hve-core-all", "developer"] } ``` -### Selecting Personas for New Artifacts +### Selecting Collections for New Artifacts -Answer these questions when determining persona assignments: +Answer these questions when determining collection assignments: 1. **Who is the primary user?** Identify the main role that benefits from this artifact 2. **Who else benefits?** Consider secondary roles that may find value -3. **Is it foundational?** Core workflow artifacts should include multiple personas -4. **Is it specialized?** Domain-specific artifacts may target fewer personas +3. **Is it foundational?** Core workflow artifacts should include multiple collections +4. **Is it specialized?** Domain-specific artifacts may target fewer collections When in doubt, include `hve-core-all` to ensure the artifact appears in the full collection while still enabling targeted distribution. @@ -234,7 +234,7 @@ Add the `requires` field to artifacts that depend on others: ```json "rpi-agent": { "maturity": "stable", - "personas": ["hve-core-all"], + "collections": ["hve-core-all"], "tags": ["rpi", "orchestration"], "requires": { "agents": ["task-researcher", "task-planner", "task-implementor", "task-reviewer"], @@ -249,7 +249,7 @@ Add the `requires` field to artifacts that depend on others: Dependency resolution currently operates at **build time** during extension packaging. The `Resolve-RequiresDependencies` function in `Prepare-Extension.ps1` walks `requires` blocks to compute the transitive closure of all dependent artifacts across types (agents, prompts, instructions, skills). Similarly, `Resolve-HandoffDependencies` performs BFS traversal of agent handoff declarations to ensure all reachable agents are included in the package. -For clone-based installations, the installer agent supports **agent-only persona filtering** in Phase 7. Full installer-side dependency resolution (automatically including required prompts, instructions, and skills based on the dependency graph) is planned for a future release. +For clone-based installations, the installer agent supports **agent-only collection filtering** in Phase 7. Full installer-side dependency resolution (automatically including required prompts, instructions, and skills based on the dependency graph) is planned for a future release. ### Dependency Best Practices diff --git a/docs/contributing/custom-agents.md b/docs/contributing/custom-agents.md index 7fd7634b..885e6373 100644 --- a/docs/contributing/custom-agents.md +++ b/docs/contributing/custom-agents.md @@ -215,11 +215,11 @@ items: maturity: stable ``` -### Selecting Personas for Agents +### Selecting Collections for Agents -Choose personas based on who benefits most from your agent: +Choose collections based on who benefits most from your agent: -| Agent Type | Recommended Personas | +| Agent Type | Recommended Collections | |----------------------|---------------------------------------| | Task workflow agents | `hve-core-all`, `developer`, `tpm` | | Architecture agents | `hve-core-all`, `architect`, `devops` | diff --git a/docs/contributing/instructions.md b/docs/contributing/instructions.md index a725080f..b5418eef 100644 --- a/docs/contributing/instructions.md +++ b/docs/contributing/instructions.md @@ -43,7 +43,7 @@ All instruction files **MUST** be placed in: ``` > [!IMPORTANT] -> The `.github/instructions/hve-core/` subdirectory is reserved for repo-specific instructions that apply only to the hve-core repository. Files in this directory are NOT registered as AI artifacts and are never distributed through extension packages or persona collections. Use this location for internal repository concerns such as CI/CD workflows or conventions that do not generalize to consumers. +> The `.github/instructions/hve-core/` subdirectory is reserved for repo-specific instructions that apply only to the hve-core repository. Files in this directory are NOT registered as AI artifacts and are never distributed through extension packages or collections. Use this location for internal repository concerns such as CI/CD workflows or conventions that do not generalize to consumers. **Examples**: @@ -146,18 +146,18 @@ items: maturity: stable ``` -### Selecting Personas for Instructions +### Selecting Collections for Instructions -Choose personas based on who uses the technology or pattern: +Choose collections based on who uses the technology or pattern: -| Instruction Type | Recommended Personas | -|-------------------------|------------------------------------------------| -| Language standards | `hve-core-all`, `developer` | -| Infrastructure (IaC) | `hve-core-all`, `architect`, `devops` | -| Documentation standards | `hve-core-all`, `technical-writer` | -| Workflow instructions | `hve-core-all` plus relevant workflow personas | -| Test standards | `hve-core-all`, `developer` | -| ADO integration | `hve-core-all`, `tpm`, `devops` | +| Instruction Type | Recommended Collections | +|-------------------------|---------------------------------------------------| +| Language standards | `hve-core-all`, `developer` | +| Infrastructure (IaC) | `hve-core-all`, `architect`, `devops` | +| Documentation standards | `hve-core-all`, `technical-writer` | +| Workflow instructions | `hve-core-all` plus relevant workflow collections | +| Test standards | `hve-core-all`, `developer` | +| ADO integration | `hve-core-all`, `tpm`, `devops` | ### Tags for Instructions diff --git a/docs/contributing/prompts.md b/docs/contributing/prompts.md index 42b9b045..dc38409b 100644 --- a/docs/contributing/prompts.md +++ b/docs/contributing/prompts.md @@ -127,18 +127,18 @@ items: maturity: stable ``` -### Selecting Personas for Prompts - -Choose personas based on who invokes or benefits from the workflow: - -| Prompt Type | Recommended Personas | -|-------------------------|-------------------------------------------| -| Git/PR workflows | `hve-core-all`, `developer` | -| ADO work item workflows | `hve-core-all`, `tpm`, `devops` | -| GitHub issue workflows | `hve-core-all`, `developer` | -| RPI workflow prompts | `hve-core-all` plus all relevant personas | -| Documentation workflows | `hve-core-all`, `technical-writer` | -| Architecture prompts | `hve-core-all`, `architect` | +### Selecting Collections for Prompts + +Choose collections based on who invokes or benefits from the workflow: + +| Prompt Type | Recommended Collections | +|-------------------------|----------------------------------------------| +| Git/PR workflows | `hve-core-all`, `developer` | +| ADO work item workflows | `hve-core-all`, `tpm`, `devops` | +| GitHub issue workflows | `hve-core-all`, `developer` | +| RPI workflow prompts | `hve-core-all` plus all relevant collections | +| Documentation workflows | `hve-core-all`, `technical-writer` | +| Architecture prompts | `hve-core-all`, `architect` | ### Tags for Prompts diff --git a/docs/contributing/skills.md b/docs/contributing/skills.md index 8d745dbe..2857af38 100644 --- a/docs/contributing/skills.md +++ b/docs/contributing/skills.md @@ -105,11 +105,11 @@ items: maturity: stable ``` -### Selecting Personas for Skills +### Selecting Collections for Skills -Choose personas based on who uses the skill's utilities: +Choose collections based on who uses the skill's utilities: -| Skill Type | Recommended Personas | +| Skill Type | Recommended Collections | |----------------------|---------------------------------------| | Media processing | `hve-core-all` | | Documentation tools | `hve-core-all`, `technical-writer` | diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 4f704d13..4467145d 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -87,7 +87,7 @@ Answer these questions to find your recommended installation method: ## Collection Packages -HVE-Core supports persona-based artifact collections tailored to specific roles: +HVE-Core supports role-based artifact collections tailored to specific roles: | Collection | Extension Name | Registry ID | Maturity | Description | |---------------|-----------------|----------------|--------------|--------------------------------------| @@ -101,18 +101,18 @@ HVE-Core supports persona-based artifact collections tailored to specific roles: The VS Code Marketplace extension installs the **full collection** containing all stable artifacts. This is the recommended approach for most users. -### Clone Methods (Persona Filtering) +### Clone Methods (Collection Filtering) -Clone-based installation methods support persona-based agent filtering through the installer agent: +Clone-based installation methods support collection-based agent filtering through the installer agent: 1. Clone the repository using your preferred method 2. Run the `hve-core-installer` agent 3. In Phase 7 (Agent Customization), select your role-based collection or install all agents -The installer reads persona assignments from the artifact registry and copies only the agents assigned to your selected persona. Agents marked for all personas are always included. +The installer reads collection assignments from the artifact registry and copies only the agents assigned to your selected collection. Agents marked for all collections are always included. > [!NOTE] -> Persona filtering applies to agents only. Copying of related prompts, instructions, and skills based on persona is planned for a future release. +> Collection filtering applies to agents only. Copying of related prompts, instructions, and skills based on collection is planned for a future release. ### Quick Decision Tree diff --git a/extension/.vscodeignore b/extension/.vscodeignore index ac649063..7d854cba 100644 --- a/extension/.vscodeignore +++ b/extension/.vscodeignore @@ -14,8 +14,8 @@ !LICENSE !CHANGELOG.md -# Exclude persona-specific READMEs (only the canonical README.md ships) +# Exclude collection-specific READMEs (only the canonical README.md ships) README.*.md -# Exclude persona-specific package templates (only the canonical package.json ships) +# Exclude collection-specific package templates (only the canonical package.json ships) package.*.json diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index 551ad8eb..65e79d0c 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -84,7 +84,7 @@ flowchart LR ### Artifact Discovery and Resolution -The prepare step discovers all artifact files on disk, filters them through the registry, and optionally narrows the set to a specific persona collection. Dependency resolution ensures transitive handoff and requires references pull in all needed artifacts. +The prepare step discovers all artifact files on disk, filters them through the registry, and optionally narrows the set to a specific collection. Dependency resolution ensures transitive handoff and requires references pull in all needed artifacts. ```mermaid flowchart TB @@ -106,7 +106,7 @@ flowchart TB FM --> CF{"Collection
specified?"} CF -->|No| FINAL[Final Artifact Set] - CF -->|Yes| PA["Filter by persona + globs
Get-CollectionArtifacts"] + CF -->|Yes| PA["Filter by collection + globs
Get-CollectionArtifacts"] PA --> HD["Resolve Handoff Closure
BFS through agent frontmatter"] HD --> RD["Resolve Requires Dependencies
BFS through registry requires"] RD --> INT["Intersect with
discovered artifacts"] @@ -192,7 +192,7 @@ flowchart TB MODE -->|"Full (default)"| FULL["Copy entire .github/
+ scripts/dev-tools/
+ scripts/lib/Modules/CIHelpers.psm1
+ docs/templates/
+ .github/skills/"] MODE -->|Collection| COLL["Copy only artifacts listed
in package.json contributes
+ scripts/dev-tools/
+ scripts/lib/Modules/CIHelpers.psm1
+ docs/templates/"] - FULL --> RDM{"Persona
README?"} + FULL --> RDM{"Collection
README?"} COLL --> RDM RDM -->|Yes| SWAP["Swap README.md
with README.{id}.md"] RDM -->|No| VSCE @@ -218,7 +218,7 @@ rm -rf .github scripts && cp -r ../.github . && mkdir -p scripts && cp -r ../scr ## Publishing the Extension -**Important:** Versions are managed by `release-please` via `extension/templates/package.template.json`. The `Prepare-Extension.ps1` script generates all persona package files with the correct version before preparing the extension. +**Important:** Versions are managed by `release-please` via `extension/templates/package.template.json`. The `Prepare-Extension.ps1` script generates all collection package files with the correct version before preparing the extension. **Setup Personal Access Token (one-time):** @@ -351,7 +351,7 @@ See [Agent Maturity Levels](../docs/contributing/ai-artifacts-common.md#maturity ## Collection-Based Packaging -The extension supports building persona-specific collection packages from a single codebase. +The extension supports building collection-specific packages from a single codebase. ### Available Collections @@ -359,22 +359,22 @@ Collection manifests are defined in root `collections/` as YAML files: | Collection | Manifest | Description | |------------|-------------------------------|----------------------------------------| -| Full | `hve-core-all.collection.yml` | All artifacts regardless of persona | +| Full | `hve-core-all.collection.yml` | All artifacts regardless of collection | | Developer | `developer.collection.yml` | Software engineering focused artifacts | -### Persona Package Files +### Collection Package Files -All persona package files (`extension/package.json`, `extension/package.*.json`) are generated by `Prepare-Extension.ps1` from the source template and root collection YAML metadata. These files are gitignored build artifacts. +All collection package files (`extension/package.json`, `extension/package.*.json`) are generated by `Prepare-Extension.ps1` from the source template and root collection YAML metadata. These files are gitignored build artifacts. -| Generated File | Source Collection | Purpose | -|---------------------|-------------------------------|--------------------------| -| `package.json` | `hve-core-all.collection.yml` | Full bundle manifest | -| `package.{id}.json` | `{id}.collection.yml` | Persona edition metadata | +| Generated File | Source Collection | Purpose | +|---------------------|-------------------------------|-----------------------------| +| `package.json` | `hve-core-all.collection.yml` | Full bundle manifest | +| `package.{id}.json` | `{id}.collection.yml` | Collection edition metadata | -When building a persona collection, `Prepare-Extension.ps1`: +When building a non-default collection, `Prepare-Extension.ps1`: 1. Backs up `package.json` to `package.json.bak` -2. Copies the persona template (`package.{id}.json`) over `package.json` +2. Copies the collection template (`package.{id}.json`) over `package.json` 3. Generates `contributes` into the copied file 4. Serializes the result as `package.json` @@ -382,7 +382,7 @@ After packaging, `Package-Extension.ps1` restores the canonical `package.json` f #### Version Synchronization -`release-please` manages the version in `extension/templates/package.template.json`. The `Prepare-Extension.ps1` script generates all persona package files with the propagated version. No manual version updates are needed. +`release-please` manages the version in `extension/templates/package.template.json`. The `Prepare-Extension.ps1` script generates all collection package files with the propagated version. No manual version updates are needed. ### Building Collection Packages @@ -393,12 +393,12 @@ To build a specific collection package: pwsh ./scripts/extension/Prepare-Extension.ps1 pwsh ./scripts/extension/Package-Extension.ps1 -# Build a persona-specific collection (copies persona template) +# Build a collection-specific package (copies collection template) pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection collections/developer.collection.yml pwsh ./scripts/extension/Package-Extension.ps1 -Collection collections/developer.collection.yml ``` -When `-Collection` targets a persona other than `hve-core-all`, the prepare script copies the persona template to `package.json` before generating `contributes`. The packaging script restores the canonical `package.json` after building. +When `-Collection` targets a collection other than `hve-core-all`, the prepare script copies the collection template to `package.json` before generating `contributes`. The packaging script restores the canonical `package.json` after building. ### Inner Dev Loop @@ -419,12 +419,12 @@ Generated package files are gitignored. Each `Prepare-Extension.ps1` invocation ### Collection Resolution -When building a collection, the system applies a multi-stage filter pipeline: persona matching, maturity gating, optional glob patterns, and two rounds of dependency resolution. +When building a collection, the system applies a multi-stage filter pipeline: collection matching, maturity gating, optional glob patterns, and two rounds of dependency resolution. ```mermaid flowchart TB - REG["Registry Entry
personas ยท maturity ยท requires"] --> PF{"Persona match?
empty personas = universal"} - CM["Collection Manifest
personas array"] --> PF + REG["Registry Entry
items ยท maturity ยท requires"] --> PF{"Collection match?
empty items = universal"} + CM["Collection Manifest
items array"] --> PF CH["Channel
Stable / PreRelease"] --> MF PF -->|Yes| MF{"Maturity
allowed?"} @@ -443,7 +443,7 @@ flowchart TB Key behaviors: -* Artifacts with an empty `personas` array are universal and included in every collection +* Artifacts with an empty `items` array are universal and included in every collection * Handoff targets bypass maturity filtering by design (an agent must be able to hand off to its declared targets) * The `requires` block supports transitive resolution: if agent A requires agent B, and B requires instruction C, all three are included * Optional `include` and `exclude` glob arrays in the collection manifest provide fine-grained control per artifact type @@ -471,14 +471,14 @@ pwsh ./scripts/extension/Package-Extension.ps1 -Version "1.0.0-test" -WhatIf **Missing artifacts in collection:** 1. Verify the artifact has a registry entry in `.github/ai-artifacts-registry.json` -2. Check the `personas` array includes the collection's persona or `hve-core-all` +2. Check the `items` array includes the collection's identifier or `hve-core-all` 3. Run `npm run lint:registry` to validate registry consistency **Dependency not included:** 1. Check the parent artifact's `requires` field in the registry 2. Ensure dependent artifacts exist and have valid registry entries -3. Dependencies are included regardless of persona filter +3. Dependencies are included regardless of collection filter **Validation errors:** @@ -513,7 +513,7 @@ items: | `displayName` | Yes | Marketplace display name | | `description` | Yes | Marketplace description text | | `maturity` | No | Release channel eligibility (`stable`, `preview`, `experimental`, `deprecated`). Defaults to `stable` | -| `personas` | Yes | Array of persona identifiers to include | +| `items` | Yes | Array of collection identifiers to include | #### Collection Maturity and Channel Eligibility @@ -532,15 +532,15 @@ Omitting the `maturity` field defaults to `stable`, maintaining backward compati ### Adding New Collections -To create a new persona collection: +To create a new collection: 1. Create a new collection manifest in `collections/`: ```yaml - id: my-persona - name: hve-my-persona - displayName: "HVE Core - My Persona Edition" - description: "Description of artifacts included for this persona" + id: my-collection + name: hve-my-collection + displayName: "HVE Core - My Collection Edition" + description: "Description of artifacts included for this collection" maturity: experimental items: - kind: agent @@ -548,9 +548,9 @@ To create a new persona collection: maturity: experimental ``` -2. Add the persona to the registry's `personas` section -3. Tag relevant artifacts with the new persona in the registry -4. Test the build locally with `-Collection collections/my-persona.collection.yml` +2. Add the collection to the registry's `items` section +3. Tag relevant artifacts with the new collection in the registry +4. Test the build locally with `-Collection collections/my-collection.collection.yml` 5. Submit PR with the new collection manifest > [!TIP] diff --git a/scripts/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index b8099bce..bc168a36 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -253,21 +253,21 @@ function New-PackagingResult { } } -function Get-PersonaReadmePath { +function Get-CollectionReadmePath { <# .SYNOPSIS - Resolves the persona-specific README path from a collection manifest. + Resolves the collection-specific README path from a collection manifest. .DESCRIPTION - Maps a collection manifest to its persona-specific README file. Returns + Maps a collection manifest to its collection-specific README file. Returns null when the collection is the full package (hve-core-all) or when no - matching persona README exists on disk. Supports both YAML and JSON + matching collection README exists on disk. Supports both YAML and JSON manifest formats. .PARAMETER CollectionPath Path to the collection manifest file (YAML or JSON). .PARAMETER ExtensionDirectory Path to the extension directory containing README files. .OUTPUTS - String path to the persona README, or $null if not applicable. + String path to the collection README, or $null if not applicable. #> [CmdletBinding()] [OutputType([string])] @@ -293,9 +293,9 @@ function Get-PersonaReadmePath { return $null } - $personaReadmePath = Join-Path $ExtensionDirectory "README.$collectionId.md" - if (Test-Path $personaReadmePath) { - return $personaReadmePath + $collectionReadmePath = Join-Path $ExtensionDirectory "README.$collectionId.md" + if (Test-Path $collectionReadmePath) { + return $collectionReadmePath } return $null @@ -567,19 +567,19 @@ function Copy-CollectionArtifacts { } } -function Set-PersonaReadme { +function Set-CollectionReadme { <# .SYNOPSIS - Swaps or restores the persona-specific README for collection packaging. + Swaps or restores the collection-specific README for extension packaging. .DESCRIPTION - In swap mode, backs up the original README.md and copies the persona + In swap mode, backs up the original README.md and copies the collection README in its place. In restore mode, copies the backup back and removes it. .PARAMETER ExtensionDirectory Path to the extension directory. - .PARAMETER PersonaReadmePath - Path to the persona-specific README file. Required for Swap operation. + .PARAMETER CollectionReadmePath + Path to the collection-specific README file. Required for Swap operation. .PARAMETER Operation - Either 'Swap' to replace README.md with persona content, or 'Restore' + Either 'Swap' to replace README.md with collection content, or 'Restore' to revert README.md from backup. #> [CmdletBinding()] @@ -588,7 +588,7 @@ function Set-PersonaReadme { [string]$ExtensionDirectory, [Parameter(Mandatory = $false)] - [string]$PersonaReadmePath = "", + [string]$CollectionReadmePath = "", [Parameter(Mandatory = $true)] [ValidateSet('Swap', 'Restore')] @@ -599,13 +599,13 @@ function Set-PersonaReadme { $backupPath = Join-Path $ExtensionDirectory "README.md.bak" if ($Operation -eq 'Swap') { - if (-not $PersonaReadmePath -or $PersonaReadmePath -eq "") { - Write-Warning "No persona README path provided for swap operation" + if (-not $CollectionReadmePath -or $CollectionReadmePath -eq "") { + Write-Warning "No collection README path provided for swap operation" return } Copy-Item -Path $readmePath -Destination $backupPath -Force - Copy-Item -Path $PersonaReadmePath -Destination $readmePath -Force - Write-Host " Swapped README.md with $(Split-Path $PersonaReadmePath -Leaf)" -ForegroundColor Green + Copy-Item -Path $CollectionReadmePath -Destination $readmePath -Force + Write-Host " Swapped README.md with $(Split-Path $CollectionReadmePath -Leaf)" -ForegroundColor Green } elseif ($Operation -eq 'Restore') { if (Test-Path $backupPath) { @@ -947,13 +947,13 @@ function Invoke-PackageExtension { Write-Host " โœ… Extension directory prepared" -ForegroundColor Green - # Swap persona README if collection specifies one + # Swap collection README if collection specifies one if ($Collection -and $Collection -ne "") { - $personaReadmePath = Get-PersonaReadmePath -CollectionPath $Collection -ExtensionDirectory $ExtensionDirectory - if ($personaReadmePath) { + $collectionReadmePath = Get-CollectionReadmePath -CollectionPath $Collection -ExtensionDirectory $ExtensionDirectory + if ($collectionReadmePath) { Write-Host "" - Write-Host "๐Ÿ“„ Applying persona README..." -ForegroundColor Yellow - Set-PersonaReadme -ExtensionDirectory $ExtensionDirectory -PersonaReadmePath $personaReadmePath -Operation Swap + Write-Host "๐Ÿ“„ Applying collection README..." -ForegroundColor Yellow + Set-CollectionReadme -ExtensionDirectory $ExtensionDirectory -CollectionReadmePath $collectionReadmePath -Operation Swap } } @@ -1022,7 +1022,7 @@ function Invoke-PackageExtension { return New-PackagingResult -Success $false -ErrorMessage $_.Exception.Message } finally { - # Restore canonical package.json from persona template backup + # Restore canonical package.json from collection template backup $backupPath = Join-Path $ExtensionDirectory "package.json.bak" if (Test-Path $backupPath) { Copy-Item -Path $backupPath -Destination $PackageJsonPath -Force @@ -1033,8 +1033,8 @@ function Invoke-PackageExtension { $packageJson = Get-Content -Path $PackageJsonPath -Raw | ConvertFrom-Json } - # Restore persona README if it was swapped - Set-PersonaReadme -ExtensionDirectory $ExtensionDirectory -Operation Restore + # Restore collection README if it was swapped + Set-CollectionReadme -ExtensionDirectory $ExtensionDirectory -Operation Restore # Cleanup copied directories using I/O function Write-Host "" diff --git a/scripts/extension/Prepare-Extension.ps1 b/scripts/extension/Prepare-Extension.ps1 index dfe62cd9..c2c84bd1 100644 --- a/scripts/extension/Prepare-Extension.ps1 +++ b/scripts/extension/Prepare-Extension.ps1 @@ -179,10 +179,10 @@ function Set-JsonFile { function Remove-StaleGeneratedFiles { <# .SYNOPSIS - Removes generated persona package files that are no longer expected. + Removes generated collection package files that are no longer expected. .DESCRIPTION Scans extension/ for package.*.json files and removes any not in the - expected set, keeping the directory clean of orphaned persona templates. + expected set, keeping the directory clean of orphaned collection templates. .PARAMETER RepoRoot Repository root path. .PARAMETER ExpectedFiles @@ -215,11 +215,11 @@ function Remove-StaleGeneratedFiles { function Invoke-ExtensionCollectionsGeneration { <# .SYNOPSIS - Generates persona package files from root collection manifests. + Generates collection package files from root collection manifests. .DESCRIPTION Reads the package template and each collections/*.collection.yml file, producing extension/package.json (for hve-core-all) and - extension/package.{id}.json for every other collection. Stale persona + extension/package.{id}.json for every other collection. Stale collection files are removed. .PARAMETER RepoRoot Repository root path containing collections/ and extension/templates/. @@ -297,7 +297,7 @@ function Invoke-ExtensionCollectionsGeneration { Remove-StaleGeneratedFiles -RepoRoot $RepoRoot -ExpectedFiles $expectedFiles - # Generate README files for each persona collection + # Generate README files for each collection $readmeTemplatePath = Join-Path $templatesDir 'README.template.md' foreach ($collectionFile in $collectionFiles) { $collection = ConvertFrom-Yaml -Yaml (Get-Content -Path $collectionFile.FullName -Raw) @@ -587,13 +587,13 @@ function Get-CollectionManifest { .SYNOPSIS Loads a collection manifest from a YAML or JSON file. .DESCRIPTION - Reads and parses a collection manifest file that defines persona-based + Reads and parses a collection manifest file that defines collection-based artifact filtering rules for extension packaging. Supports both YAML (.yml/.yaml) and JSON (.json) formats. .PARAMETER CollectionPath Path to the collection manifest file (YAML or JSON). .OUTPUTS - [hashtable] Parsed collection manifest with id, name, displayName, description, personas, and optional include/exclude. + [hashtable] Parsed collection manifest with id, name, displayName, description, items, and optional include/exclude. #> [CmdletBinding()] [OutputType([hashtable])] @@ -1410,14 +1410,14 @@ function New-PrepareResult { function Test-TemplateConsistency { <# .SYNOPSIS - Validates persona template metadata against its collection manifest. + Validates collection template metadata against its collection manifest. .DESCRIPTION - Compares name, displayName, and description fields between a persona + Compares name, displayName, and description fields between a collection package template (e.g. package.developer.json) and the corresponding collection manifest. Emits warnings for divergences and returns a list of mismatches. .PARAMETER TemplatePath - Path to the persona package template JSON file. + Path to the collection package template JSON file. .PARAMETER CollectionManifest Parsed collection manifest hashtable with name, displayName, description. .OUTPUTS @@ -1541,12 +1541,12 @@ function Invoke-PrepareExtension { $GitHubDir = Join-Path $RepoRoot ".github" $PackageJsonPath = Join-Path $ExtensionDirectory "package.json" - # Generate persona package files from root collection manifests. + # Generate collection package files from root collection manifests. # This ensures extension/package.json and extension/package.*.json exist # with the correct version from the template before any reads occur. try { $generated = Invoke-ExtensionCollectionsGeneration -RepoRoot $RepoRoot - Write-Host "Generated $($generated.Count) persona package file(s)" -ForegroundColor Green + Write-Host "Generated $($generated.Count) collection package file(s)" -ForegroundColor Green } catch { return New-PrepareResult -Success $false -ErrorMessage "Package generation failed: $($_.Exception.Message)" @@ -1752,12 +1752,12 @@ function Invoke-PrepareExtension { Write-Host "Skills after filter: $($chatSkills.Count)" } - # Apply persona template when building a non-default collection + # Apply collection template when building a non-default collection if ($null -ne $collectionManifest -and $collectionManifest.id -ne 'hve-core-all') { $collectionId = $collectionManifest.id $templatePath = Join-Path $ExtensionDirectory "package.$collectionId.json" if (-not (Test-Path $templatePath)) { - return New-PrepareResult -Success $false -ErrorMessage "Persona template not found: $templatePath" + return New-PrepareResult -Success $false -ErrorMessage "Collection template not found: $templatePath" } # Validate template consistency against collection manifest @@ -1774,12 +1774,12 @@ function Invoke-PrepareExtension { $backupPath = Join-Path $ExtensionDirectory "package.json.bak" Copy-Item -Path $PackageJsonPath -Destination $backupPath -Force - # Copy persona template over package.json + # Copy collection template over package.json Copy-Item -Path $templatePath -Destination $PackageJsonPath -Force # Re-read template as the working package.json $packageJson = Get-Content -Path $PackageJsonPath -Raw | ConvertFrom-Json - Write-Host "Applied persona template: package.$collectionId.json" -ForegroundColor Green + Write-Host "Applied collection template: package.$collectionId.json" -ForegroundColor Green } # Update package.json with generated contributes diff --git a/scripts/linting/schemas/collection.schema.json b/scripts/linting/schemas/collection.schema.json deleted file mode 100644 index 5d41b792..00000000 --- a/scripts/linting/schemas/collection.schema.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/microsoft/hve-core/schemas/collection.schema.json", - "title": "Collection Manifest Schema", - "description": "Schema for persona-targeted extension collection manifests", - "type": "object", - "required": [ - "$schema", - "id", - "name", - "displayName", - "description", - "personas" - ], - "properties": { - "$schema": { - "type": "string" - }, - "id": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$", - "minLength": 1 - }, - "name": { - "type": "string", - "minLength": 1 - }, - "displayName": { - "type": "string", - "minLength": 1 - }, - "description": { - "type": "string", - "minLength": 1 - }, - "maturity": { - "type": "string", - "enum": [ - "stable", - "preview", - "experimental", - "deprecated" - ], - "default": "stable", - "description": "Collection-level maturity controlling release channel eligibility. Experimental collections are released only as PreRelease. Deprecated collections are excluded from all releases." - }, - "personas": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" - }, - "minItems": 1 - }, - "include": { - "$ref": "#/$defs/artifactGlobFilter" - }, - "exclude": { - "$ref": "#/$defs/artifactGlobFilter" - } - }, - "additionalProperties": false, - "$defs": { - "artifactGlobFilter": { - "type": "object", - "properties": { - "agents": { - "$ref": "#/$defs/globPatternArray" - }, - "prompts": { - "$ref": "#/$defs/globPatternArray" - }, - "instructions": { - "$ref": "#/$defs/globPatternArray" - }, - "skills": { - "$ref": "#/$defs/globPatternArray" - } - }, - "additionalProperties": false - }, - "globPatternArray": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - } - } - } -} \ No newline at end of file diff --git a/scripts/tests/extension/Package-Extension.Tests.ps1 b/scripts/tests/extension/Package-Extension.Tests.ps1 index d7e9b09d..69a53da2 100644 --- a/scripts/tests/extension/Package-Extension.Tests.ps1 +++ b/scripts/tests/extension/Package-Extension.Tests.ps1 @@ -563,17 +563,17 @@ Describe 'Invoke-PackageExtension' { # Template manifest currently in package.json $templateManifest = @{ - name = 'hve-persona' + name = 'hve-test-collection' version = '2.5.0' - publisher = 'persona-pub' - description = 'Persona description' + publisher = 'test-pub' + description = 'Test description' engines = @{ vscode = '^1.80.0' } } $templateManifest | ConvertTo-Json | Set-Content (Join-Path $script:extDir 'package.json') $originalManifest | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $script:extDir 'package.json.bak') # Create fake vsix matching the template name - $vsixPath = Join-Path $script:extDir 'hve-persona-2.5.0.vsix' + $vsixPath = Join-Path $script:extDir 'hve-test-collection-2.5.0.vsix' Set-Content -Path $vsixPath -Value 'fake-vsix' $null = Invoke-PackageExtension -ExtensionDirectory $script:extDir -RepoRoot $script:repoRoot @@ -921,9 +921,9 @@ Describe 'Restore-PackageJsonVersion' { } } -Describe 'Get-PersonaReadmePath' { +Describe 'Get-CollectionReadmePath' { BeforeAll { - $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "persona-readme-test-$([guid]::NewGuid().ToString('N').Substring(0,8))" + $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "collection-readme-test-$([guid]::NewGuid().ToString('N').Substring(0,8))" $script:extDir = Join-Path $script:testDir 'extension' } @@ -944,37 +944,37 @@ id: hve-core-all name: all "@ | Set-Content $collectionPath - $result = Get-PersonaReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir + $result = Get-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir $result | Should -BeNullOrEmpty } - It 'Returns persona README path when file exists' { + It 'Returns collection README path when file exists' { $collectionPath = Join-Path $script:testDir 'collection.yml' @" id: developer name: dev "@ | Set-Content $collectionPath - $personaReadme = Join-Path $script:extDir 'README.developer.md' - Set-Content -Path $personaReadme -Value '# Developer README' + $collectionReadme = Join-Path $script:extDir 'README.developer.md' + Set-Content -Path $collectionReadme -Value '# Developer README' - $result = Get-PersonaReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir - $result | Should -Be $personaReadme + $result = Get-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir + $result | Should -Be $collectionReadme } - It 'Returns null when persona README file does not exist' { + It 'Returns null when collection README file does not exist' { $collectionPath = Join-Path $script:testDir 'collection.yml' @" id: security name: sec "@ | Set-Content $collectionPath - $result = Get-PersonaReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir + $result = Get-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir $result | Should -BeNullOrEmpty } } -Describe 'Set-PersonaReadme' { +Describe 'Set-CollectionReadme' { BeforeAll { $script:testDir = Join-Path ([System.IO.Path]::GetTempPath()) "set-readme-test-$([guid]::NewGuid().ToString('N').Substring(0,8))" } @@ -990,11 +990,11 @@ Describe 'Set-PersonaReadme' { } } - It 'Swaps README.md with persona README and creates backup' { - $personaPath = Join-Path $script:testDir 'README.developer.md' - Set-Content -Path $personaPath -Value '# Developer README' + It 'Swaps README.md with collection README and creates backup' { + $collectionReadmePath = Join-Path $script:testDir 'README.developer.md' + Set-Content -Path $collectionReadmePath -Value '# Developer README' - Set-PersonaReadme -ExtensionDirectory $script:testDir -PersonaReadmePath $personaPath -Operation Swap + Set-CollectionReadme -ExtensionDirectory $script:testDir -CollectionReadmePath $collectionReadmePath -Operation Swap $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw $readmeContent | Should -Match 'Developer README' @@ -1004,9 +1004,9 @@ Describe 'Set-PersonaReadme' { $backupContent | Should -Match 'Original README' } - It 'Warns and returns early when no persona path for swap' { + It 'Warns and returns early when no collection path for swap' { Mock Write-Warning {} - Set-PersonaReadme -ExtensionDirectory $script:testDir -Operation Swap + Set-CollectionReadme -ExtensionDirectory $script:testDir -Operation Swap Should -Invoke Write-Warning -Times 1 $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw @@ -1016,9 +1016,9 @@ Describe 'Set-PersonaReadme' { It 'Restores README.md from backup and removes backup file' { # Create backup state Set-Content -Path (Join-Path $script:testDir 'README.md.bak') -Value '# Original README' - Set-Content -Path (Join-Path $script:testDir 'README.md') -Value '# Persona README' + Set-Content -Path (Join-Path $script:testDir 'README.md') -Value '# Collection README' - Set-PersonaReadme -ExtensionDirectory $script:testDir -Operation Restore + Set-CollectionReadme -ExtensionDirectory $script:testDir -Operation Restore $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw $readmeContent | Should -Match 'Original README' @@ -1026,7 +1026,7 @@ Describe 'Set-PersonaReadme' { } It 'Restore is a no-op when no backup exists' { - { Set-PersonaReadme -ExtensionDirectory $script:testDir -Operation Restore } | Should -Not -Throw + { Set-CollectionReadme -ExtensionDirectory $script:testDir -Operation Restore } | Should -Not -Throw $readmeContent = Get-Content -Path (Join-Path $script:testDir 'README.md') -Raw $readmeContent | Should -Match 'Original README' } @@ -1196,7 +1196,7 @@ Describe 'Invoke-PackageExtension - Collection mode' { id: developer name: dev displayName: Developer -personas: +items: - developer "@ | Set-Content $collectionPath @@ -1207,7 +1207,7 @@ personas: $result | Should -BeOfType [hashtable] } - It 'Swaps persona README when collection has matching persona README' { + It 'Swaps collection README when collection has matching collection README' { Mock Test-VsceAvailable { return @{ IsAvailable = $true; CommandType = 'vsce'; Command = 'vsce' } } Mock Get-VscePackageCommand { return @{ Executable = 'echo'; Arguments = @('mocked') } } @@ -1216,12 +1216,12 @@ personas: id: developer name: dev displayName: Developer -personas: +items: - developer "@ | Set-Content $collectionPath - # Create persona README in extension directory - Set-Content -Path (Join-Path $script:extDir 'README.developer.md') -Value '# Developer Persona' + # Create collection README in extension directory + Set-Content -Path (Join-Path $script:extDir 'README.developer.md') -Value '# Developer Collection' $vsixPath = Join-Path $script:extDir 'test-ext-1.0.0.vsix' Set-Content -Path $vsixPath -Value 'fake-vsix' diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index aed0a6a4..f9e1f34c 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -112,7 +112,7 @@ Describe 'Remove-StaleGeneratedFiles' { Test-Path $staleFile | Should -BeFalse } - It 'Does not remove non-persona files' { + It 'Does not remove non-collection files' { $regularFile = Join-Path $script:extDir 'README.md' '# Test' | Set-Content -Path $regularFile @@ -173,7 +173,7 @@ description: RPI workflow agents $pkg.version | Should -Be '2.0.0' } - It 'Generates persona package file for non-default collection' { + It 'Generates collection package file for non-default collection' { $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir $pkgPath = Join-Path $script:tempDir 'extension/package.rpi.json' Test-Path $pkgPath | Should -BeTrue @@ -195,7 +195,7 @@ description: RPI workflow agents } } - It 'Removes stale persona files not matching current collections' { + It 'Removes stale collection files not matching current collections' { $staleFile = Join-Path $script:tempDir 'extension/package.obsolete.json' '{}' | Set-Content -Path $staleFile @@ -359,16 +359,16 @@ description: "My skill description" $content | Should -Match '\*\*my-skill\*\*.*My skill description' } - It 'Includes Full Edition link for persona collections' { + It 'Includes Full Edition link for non-default collections' { $collection = @{ - id = 'persona' - name = 'Persona' - description = 'Persona test' + id = 'test-edition' + name = 'Test Edition' + description = 'Test edition test' items = @() } - $mdPath = Join-Path $script:tempDir 'persona.collection.md' - 'Persona body.' | Set-Content -Path $mdPath - $outPath = Join-Path $script:tempDir 'README.persona.md' + $mdPath = Join-Path $script:tempDir 'test-edition.collection.md' + 'Test edition body.' | Set-Content -Path $mdPath + $outPath = Join-Path $script:tempDir 'README.test-edition.md' New-CollectionReadme -Collection $collection -CollectionMdPath $mdPath -TemplatePath $script:templatePath -RepoRoot $script:tempDir -OutputPath $outPath @@ -779,7 +779,7 @@ id: test name: test-ext displayName: Test Extension description: Test -personas: +items: - hve-core-all "@ | Set-Content -Path $manifestFile @@ -791,12 +791,12 @@ personas: It 'Loads collection manifest from valid JSON path' { $manifestFile = Join-Path $script:tempDir 'test.collection.json' @{ - '\$schema' = '../schemas/collection.schema.json' + '\$schema' = '../schemas/collection-manifest.schema.json' id = 'test' name = 'test-ext' displayName = 'Test Extension' description = 'Test' - personas = @('hve-core-all') + items = @('hve-core-all') } | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestFile $result = Get-CollectionManifest -CollectionPath $manifestFile @@ -816,14 +816,14 @@ id: keys name: keys-ext displayName: Keys description: Keys test -personas: +items: - developer "@ | Set-Content -Path $manifestFile $result = Get-CollectionManifest -CollectionPath $manifestFile $result.Keys | Should -Contain 'id' $result.Keys | Should -Contain 'name' - $result.Keys | Should -Contain 'personas' + $result.Keys | Should -Contain 'items' } } @@ -1208,8 +1208,6 @@ id: hve-core-all name: hve-core-all displayName: HVE Core - All description: Channel filtering test -personas: - - hve-core-all items: - kind: agent path: .github/agents/test.agent.md @@ -1258,8 +1256,6 @@ id: hve-core-all name: hve-core-all displayName: HVE Core - All description: Prompt/instruction filtering test -personas: - - hve-core-all items: - kind: agent path: .github/agents/test.agent.md @@ -1361,7 +1357,7 @@ id: test $result.ErrorMessage | Should -Match 'Package generation failed' } - Context 'Persona template copy' { + Context 'Collection template copy' { BeforeAll { # Developer collection manifest (in collections/ for generation) $script:devCollectionYaml = Join-Path $script:collectionsDir 'developer.collection.yml' @@ -1370,8 +1366,6 @@ id: developer name: hve-developer displayName: HVE Core - Developer Edition description: Developer edition -personas: - - developer "@ | Set-Content -Path $script:devCollectionYaml $script:devCollectionPath = $script:devCollectionYaml @@ -1382,8 +1376,6 @@ id: hve-core-all name: hve-core-all displayName: HVE Core - All description: All artifacts -personas: - - hve-core-all "@ | Set-Content -Path $script:allCollectionPath # Collection manifest referencing a missing template @@ -1393,14 +1385,12 @@ id: nonexistent name: nonexistent displayName: Nonexistent description: Missing template -personas: - - nonexistent "@ | Set-Content -Path $script:missingCollectionPath } AfterEach { - # Clean up backup files left by persona template copy + # Clean up backup files left by collection template copy $bakPath = Join-Path $script:extDir 'package.json.bak' if (Test-Path $bakPath) { Remove-Item -Path $bakPath -Force @@ -1415,7 +1405,7 @@ personas: -DryRun $result.Success | Should -BeTrue - # package.json should contain the generated hve-core-all content (not a persona template) + # package.json should contain the generated hve-core-all content (not a collection template) $currentJson = Get-Content -Path (Join-Path $script:extDir 'package.json') -Raw | ConvertFrom-Json $currentJson.name | Should -Be 'hve-core' Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse @@ -1433,7 +1423,7 @@ personas: Test-Path (Join-Path $script:extDir 'package.json.bak') | Should -BeFalse } - It 'Returns error when persona template file missing' { + It 'Returns error when collection template file missing' { $result = Invoke-PrepareExtension ` -ExtensionDirectory $script:extDir ` -RepoRoot $script:tempDir ` @@ -1442,7 +1432,7 @@ personas: -DryRun $result.Success | Should -BeFalse - $result.ErrorMessage | Should -Match 'Persona template not found' + $result.ErrorMessage | Should -Match 'Collection template not found' } It 'Copies template to package.json for non-default collection' { @@ -1484,8 +1474,6 @@ id: deprecated-coll name: deprecated-ext displayName: Deprecated Collection description: Deprecated collection for testing -personas: - - hve-core-all maturity: deprecated "@ | Set-Content -Path $script:deprecatedCollectionPath @@ -1496,8 +1484,6 @@ id: experimental-coll name: experimental-ext displayName: Experimental Collection description: Experimental collection for testing -personas: - - hve-core-all maturity: experimental "@ | Set-Content -Path $script:experimentalCollectionPath } From cd9162b6cef0ccc5ebeb80e88cceea896c1a812b Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 13 Feb 2026 10:40:44 +0100 Subject: [PATCH 58/62] docs(agents): update terminology from 'persona' to 'collection' in agent documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - revise agent selection logic to reference collection manifests - clarify installation method descriptions in documentation - enhance consistency in collection-related comments and examples ๐Ÿ”„ - Generated by Copilot --- .github/agents/hve-core-installer.agent.md | 6 +-- docs/contributing/ai-artifacts-common.md | 54 +++++++++++----------- docs/getting-started/install.md | 4 +- extension/PACKAGING.md | 43 +++++++++-------- 4 files changed, 54 insertions(+), 53 deletions(-) diff --git a/.github/agents/hve-core-installer.agent.md b/.github/agents/hve-core-installer.agent.md index ebe91624..eb381e9c 100644 --- a/.github/agents/hve-core-installer.agent.md +++ b/.github/agents/hve-core-installer.agent.md @@ -1116,8 +1116,8 @@ User input handling: For each selected collection identifier: -1. Iterate through `agents` in the registry -2. Include agents where `maturity` is `stable` AND the collection's items list contains the agent +1. Iterate through `items` in the collection manifest +2. Include items where `kind` is `agent` AND `maturity` is `stable` 3. Deduplicate across multiple selected collections #### Step 4: Present filtered agents for confirmation @@ -1172,7 +1172,7 @@ $targetDir = ".github/agents" $filesToCopy = switch ($selection) { "rpi-core" { @("task-researcher.agent.md", "task-planner.agent.md", "task-implementor.agent.md", "task-reviewer.agent.md", "rpi-agent.agent.md") } default { - # Collection-based: $selection contains filtered agent names from registry + # Collection-based: $selection contains filtered agent names from collection manifest $collectionAgents } } diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 9b0666b8..0b235964 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -171,21 +171,20 @@ When assigning collections to artifacts: **Example collection assignments:** -```json -// Universal - available in all collections -"markdown": { - "collections": ["hve-core-all", "developer"] -} +Adding an artifact to multiple collections means adding its `items[]` entry in each relevant `collections/*.collection.yml`: -// Developer-focused - targeted distribution -"csharp/csharp": { - "collections": ["hve-core-all", "developer"] -} +```yaml +# In collections/hve-core-all.collection.yml - Universal +- path: .github/instructions/markdown.instructions.md + kind: instruction -// Core workflow - broadly applicable -"rpi-agent": { - "collections": ["hve-core-all", "developer"] -} +# In collections/developer.collection.yml - Developer-focused +- path: .github/instructions/markdown.instructions.md + kind: instruction + +# In collections/rpi.collection.yml - Core workflow +- path: .github/agents/rpi-agent.agent.md + kind: agent ``` ### Selecting Collections for New Artifacts @@ -229,20 +228,23 @@ The companion function `Resolve-RequiresDependencies` in the same script applies ### Declaring Dependencies -Add the `requires` field to artifacts that depend on others: +Add the `requires` field to collection items in `collections/*.collection.yml`: -```json -"rpi-agent": { - "maturity": "stable", - "collections": ["hve-core-all"], - "tags": ["rpi", "orchestration"], - "requires": { - "agents": ["task-researcher", "task-planner", "task-implementor", "task-reviewer"], - "prompts": ["task-research", "task-plan", "task-implement", "task-review"], - "instructions": [], - "skills": [] - } -} +```yaml +- path: .github/agents/rpi-agent.agent.md + kind: agent + maturity: stable + requires: + agents: + - task-researcher + - task-planner + - task-implementor + - task-reviewer + prompts: + - task-research + - task-plan + - task-implement + - task-review ``` ### Dependency Resolution diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 4467145d..92ef7b24 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -89,7 +89,7 @@ Answer these questions to find your recommended installation method: HVE-Core supports role-based artifact collections tailored to specific roles: -| Collection | Extension Name | Registry ID | Maturity | Description | +| Collection | Extension Name | Collection ID | Maturity | Description | |---------------|-----------------|----------------|--------------|--------------------------------------| | **Full** | `hve-core` | `hve-core-all` | Stable | All artifacts (recommended for most) | | **Developer** | `hve-developer` | `developer` | Experimental | Software engineering focus | @@ -109,7 +109,7 @@ Clone-based installation methods support collection-based agent filtering throug 2. Run the `hve-core-installer` agent 3. In Phase 7 (Agent Customization), select your role-based collection or install all agents -The installer reads collection assignments from the artifact registry and copies only the agents assigned to your selected collection. Agents marked for all collections are always included. +The installer reads collection assignments from the collection manifests (`collections/*.collection.yml`) and copies only the agents assigned to your selected collection. Agents marked for all collections are always included. > [!NOTE] > Collection filtering applies to agents only. Copying of related prompts, instructions, and skills based on collection is planned for a future release. diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index 65e79d0c..935c0440 100644 --- a/extension/PACKAGING.md +++ b/extension/PACKAGING.md @@ -67,7 +67,7 @@ Extension packaging is a two-step process: **Prepare** discovers and filters art ```mermaid flowchart LR subgraph Prepare["Step 1 ยท Prepare-Extension.ps1"] - P1[Load Registry] --> P2[Discover Artifacts] + P1[Load Collection Manifests] --> P2[Discover Artifacts] P2 --> P3["Filter by Maturity
+ Collection"] P3 --> P4[Resolve Dependencies] P4 --> P5[Write package.json] @@ -84,13 +84,12 @@ flowchart LR ### Artifact Discovery and Resolution -The prepare step discovers all artifact files on disk, filters them through the registry, and optionally narrows the set to a specific collection. Dependency resolution ensures transitive handoff and requires references pull in all needed artifacts. +The prepare step generates collection package files from `collections/*.collection.yml` manifests, discovers all artifact files on disk, filters them by maturity and collection membership, and resolves transitive handoff and requires dependencies to pull in all needed artifacts. ```mermaid flowchart TB - REG[ai-artifacts-registry.json] -->|Get-RegistryData| INPUTS + CM["Collection Manifests
collections/*.collection.yml"] -->|Get-CollectionManifest| INPUTS CH[Channel: Stable / PreRelease] -->|Get-AllowedMaturities| INPUTS - CM["Collection Manifest
optional"] -->|Get-CollectionManifest| INPUTS INPUTS[Resolve Inputs] --> DISC[Discover Artifact Files from .github/] @@ -108,7 +107,7 @@ flowchart TB CF -->|No| FINAL[Final Artifact Set] CF -->|Yes| PA["Filter by collection + globs
Get-CollectionArtifacts"] PA --> HD["Resolve Handoff Closure
BFS through agent frontmatter"] - HD --> RD["Resolve Requires Dependencies
BFS through registry requires"] + HD --> RD["Resolve Requires Dependencies
BFS through collection item requires"] RD --> INT["Intersect with
discovered artifacts"] INT --> FINAL @@ -340,7 +339,7 @@ The workflow validates the version is ODD before proceeding. ### Agent Maturity Filtering -When packaging, artifacts are filtered by their `maturity` field in `.github/ai-artifacts-registry.json`: +When packaging, artifacts are filtered by their `maturity` field in `collections/*.collection.yml` item entries: | Channel | Included Maturity Levels | |------------|-------------------------------------| @@ -423,7 +422,7 @@ When building a collection, the system applies a multi-stage filter pipeline: co ```mermaid flowchart TB - REG["Registry Entry
items ยท maturity ยท requires"] --> PF{"Collection match?
empty items = universal"} + CI["Collection Item
path ยท kind ยท maturity ยท requires"] --> PF{"Collection match?
empty items = universal"} CM["Collection Manifest
items array"] --> PF CH["Channel
Stable / PreRelease"] --> MF @@ -437,7 +436,7 @@ flowchart TB GLOB -->|No| EXCLUDE SEED --> HANDOFF["Resolve Handoff Closure
BFS through agent frontmatter
handoff targets bypass maturity filter"] - HANDOFF --> REQUIRES["Resolve Requires Dependencies
BFS through registry requires blocks
across agents ยท prompts ยท instructions ยท skills"] + HANDOFF --> REQUIRES["Resolve Requires Dependencies
BFS through collection item requires blocks
across agents ยท prompts ยท instructions ยท skills"] REQUIRES --> FINAL[Final Collection Artifact Set] ``` @@ -445,7 +444,7 @@ Key behaviors: * Artifacts with an empty `items` array are universal and included in every collection * Handoff targets bypass maturity filtering by design (an agent must be able to hand off to its declared targets) -* The `requires` block supports transitive resolution: if agent A requires agent B, and B requires instruction C, all three are included +* The `requires` block in collection items supports transitive resolution: if agent A requires agent B, and B requires instruction C, all three are included * Optional `include` and `exclude` glob arrays in the collection manifest provide fine-grained control per artifact type ### Testing Collection Builds Locally @@ -459,8 +458,8 @@ pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection collections/developer # 2. Check package.json for included artifacts cat extension/package.json | jq '.contributes.chatAgents' -# 3. Validate the registry -npm run lint:registry +# 3. Validate collection metadata +npm run lint:collections-metadata # 4. Build the package (dry run) pwsh ./scripts/extension/Package-Extension.ps1 -Version "1.0.0-test" -WhatIf @@ -470,24 +469,24 @@ pwsh ./scripts/extension/Package-Extension.ps1 -Version "1.0.0-test" -WhatIf **Missing artifacts in collection:** -1. Verify the artifact has a registry entry in `.github/ai-artifacts-registry.json` -2. Check the `items` array includes the collection's identifier or `hve-core-all` -3. Run `npm run lint:registry` to validate registry consistency +1. Verify the artifact has an `items[]` entry in the relevant `collections/*.collection.yml` manifest +2. Check the collection manifest includes the artifact with the correct `kind` and `path` +3. Run `npm run lint:collections-metadata` to validate collection consistency **Dependency not included:** -1. Check the parent artifact's `requires` field in the registry -2. Ensure dependent artifacts exist and have valid registry entries +1. Check the parent artifact's `requires` field in the collection item +2. Ensure dependent artifacts exist and have valid collection entries 3. Dependencies are included regardless of collection filter **Validation errors:** ```bash -# Run full registry validation -npm run lint:registry +# Run full collection metadata validation +npm run lint:collections-metadata -# Check for orphaned artifacts (in registry but no file) -npm run lint:registry -- --verbose +# Validate YAML syntax of collection manifests +npm run lint:yaml ``` ### Collection Manifest Schema @@ -548,8 +547,8 @@ To create a new collection: maturity: experimental ``` -2. Add the collection to the registry's `items` section -3. Tag relevant artifacts with the new collection in the registry +2. Add artifact entries to the `items` array in the manifest +3. Set `kind`, `path`, and optionally `maturity` for each item 4. Test the build locally with `-Collection collections/my-collection.collection.yml` 5. Submit PR with the new collection manifest From 313991a55d263d257642551bb85e4a289104de23 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 13 Feb 2026 10:51:36 +0100 Subject: [PATCH 59/62] feat(docs): update collection recommendations and add tags for better categorization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - revise recommended collections for various agent types - introduce tags for collections to enhance discoverability - update documentation for skills and instructions to reflect new standards ๐Ÿ”– - Generated by Copilot --- docs/contributing/ai-artifacts-common.md | 44 ++++++++++++++++++++---- docs/contributing/custom-agents.md | 16 ++++----- docs/contributing/instructions.md | 23 +++---------- docs/contributing/prompts.md | 28 ++++----------- docs/contributing/skills.md | 25 ++++---------- 5 files changed, 64 insertions(+), 72 deletions(-) diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 0b235964..08cefd3d 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -105,6 +105,10 @@ Each manifest contains top-level collection metadata and an `items` array: id: coding-standards name: Coding Standards description: Language-specific coding instructions +tags: + - coding-standards + - bash + - python items: - path: .github/instructions/python-script.instructions.md kind: instruction @@ -114,6 +118,25 @@ items: maturity: preview ``` +### Collection Tags + +Each collection manifest declares a top-level `tags` array for categorization and discoverability. Tags exist **only at the collection level**, not on individual items. + +| Collection | Tags | +|----------------------|-------------------------------------------------------------------| +| `hve-core-all` | `hve`, `complete`, `bundle` | +| `ado` | `azure-devops`, `ado`, `work-items`, `builds`, `pull-requests` | +| `coding-standards` | `coding-standards`, `bash`, `bicep`, `csharp`, `python`, `terraform`, `uv` | +| `data-science` | `data`, `jupyter`, `streamlit`, `dashboards`, `visualization`, `data-science` | +| `git` | `git`, `commits`, `merge`, `pull-request` | +| `github` | `github`, `issues`, `backlog`, `triage`, `sprint` | +| `project-planning` | `documentation`, `architecture`, `adr`, `brd`, `prd`, `diagrams`, `planning` | +| `prompt-engineering` | `prompts`, `agents`, `authoring`, `refactoring` | +| `rpi` | `workflow`, `rpi`, `planning`, `research`, `implementation`, `review` | +| `security-planning` | `security`, `incident-response`, `risk`, `planning` | + +When creating a new collection, choose tags that describe the domain, technologies, and workflows covered. Use lowercase kebab-case and prefer existing tags before introducing new ones. + ### Collection Item Format Each `items[]` entry follows this structure: @@ -137,7 +160,8 @@ When contributing a new artifact: 1. Create the artifact file in the appropriate directory 2. Add a matching `items[]` entry in one or more `collections/*.collection.yml` files 3. Set `maturity` when the artifact should be `preview`, `experimental`, or `deprecated` -4. Run `npm run lint:yaml` to validate manifest syntax and schema compliance +4. Update the collection's `tags` array if your artifact introduces a new technology or domain not yet represented +5. Run `npm run lint:yaml` to validate manifest syntax and schema compliance ### Repo-Specific Instructions Exclusion @@ -156,10 +180,18 @@ Collections represent role-targeted artifact packages for HVE-Core artifacts. Th ### Defined Collections -| Collection | Identifier | Description | -|---------------|----------------|---------------------------------| -| **All** | `hve-core-all` | Full release with all artifacts | -| **Developer** | `developer` | Software engineers writing code | +| Collection | Identifier | Description | +|--------------------------|----------------------|--------------------------------------------------------------------------------| +| **All** | `hve-core-all` | Full bundle of all stable HVE Core agents, prompts, instructions, and skills | +| **Azure DevOps** | `ado` | Azure DevOps work item management, build monitoring, and pull request creation | +| **Coding Standards** | `coding-standards` | Language-specific coding instructions for bash, Bicep, C#, Python, and Terraform | +| **Data Science** | `data-science` | Data specification generation, Jupyter notebooks, and Streamlit dashboards | +| **Git Workflow** | `git` | Git commit messages, merges, setup, and pull request prompts | +| **GitHub Backlog** | `github` | GitHub issue discovery, triage, sprint planning, and backlog execution | +| **Project Planning** | `project-planning` | PRDs, BRDs, ADRs, architecture diagrams, and documentation operations | +| **Prompt Engineering** | `prompt-engineering` | Tools for analyzing, building, and refactoring prompts, agents, and instructions | +| **RPI Workflow** | `rpi` | Research, Plan, Implement, Review workflow agents and prompts | +| **Security Planning** | `security-planning` | Security plan creation, incident response, and risk assessment | ### Collection Assignment Guidelines @@ -178,7 +210,7 @@ Adding an artifact to multiple collections means adding its `items[]` entry in e - path: .github/instructions/markdown.instructions.md kind: instruction -# In collections/developer.collection.yml - Developer-focused +# In collections/coding-standards.collection.yml - Coding standards - path: .github/instructions/markdown.instructions.md kind: instruction diff --git a/docs/contributing/custom-agents.md b/docs/contributing/custom-agents.md index 885e6373..5d5b6c6a 100644 --- a/docs/contributing/custom-agents.md +++ b/docs/contributing/custom-agents.md @@ -219,14 +219,14 @@ items: Choose collections based on who benefits most from your agent: -| Agent Type | Recommended Collections | -|----------------------|---------------------------------------| -| Task workflow agents | `hve-core-all`, `developer`, `tpm` | -| Architecture agents | `hve-core-all`, `architect`, `devops` | -| Documentation agents | `hve-core-all`, `technical-writer` | -| Data science agents | `hve-core-all`, `developer` | -| ADO/work item agents | `hve-core-all`, `tpm`, `devops` | -| Code review agents | `hve-core-all`, `developer` | +| Agent Type | Recommended Collections | +|----------------------|------------------------------------------------| +| Task workflow agents | `hve-core-all`, `rpi` | +| Architecture agents | `hve-core-all`, `project-planning` | +| Documentation agents | `hve-core-all`, `prompt-engineering` | +| Data science agents | `hve-core-all`, `data-science` | +| ADO/work item agents | `hve-core-all`, `ado`, `project-planning` | +| Code review agents | `hve-core-all`, `coding-standards` | ### Declaring Agent Dependencies diff --git a/docs/contributing/instructions.md b/docs/contributing/instructions.md index b5418eef..dc1815da 100644 --- a/docs/contributing/instructions.md +++ b/docs/contributing/instructions.md @@ -152,25 +152,12 @@ Choose collections based on who uses the technology or pattern: | Instruction Type | Recommended Collections | |-------------------------|---------------------------------------------------| -| Language standards | `hve-core-all`, `developer` | -| Infrastructure (IaC) | `hve-core-all`, `architect`, `devops` | -| Documentation standards | `hve-core-all`, `technical-writer` | +| Language standards | `hve-core-all`, `coding-standards` | +| Infrastructure (IaC) | `hve-core-all`, `coding-standards` | +| Documentation standards | `hve-core-all`, `prompt-engineering` | | Workflow instructions | `hve-core-all` plus relevant workflow collections | -| Test standards | `hve-core-all`, `developer` | -| ADO integration | `hve-core-all`, `tpm`, `devops` | - -### Tags for Instructions - -Common tags for instructions: - -| Tag | Use For | -|------------------|----------------------------------| -| `language` | Programming language standards | -| `infrastructure` | IaC tools (Terraform, Bicep) | -| `documentation` | Writing and formatting standards | -| `testing` | Test code conventions | -| `ado` | Azure DevOps integration | -| `git` | Git workflow patterns | +| Test standards | `hve-core-all`, `coding-standards` | +| ADO integration | `hve-core-all`, `ado`, `project-planning` | For complete collection documentation, see [AI Artifacts Common Standards - Collection Manifests](ai-artifacts-common.md#collection-manifests). diff --git a/docs/contributing/prompts.md b/docs/contributing/prompts.md index dc38409b..712c036b 100644 --- a/docs/contributing/prompts.md +++ b/docs/contributing/prompts.md @@ -133,28 +133,12 @@ Choose collections based on who invokes or benefits from the workflow: | Prompt Type | Recommended Collections | |-------------------------|----------------------------------------------| -| Git/PR workflows | `hve-core-all`, `developer` | -| ADO work item workflows | `hve-core-all`, `tpm`, `devops` | -| GitHub issue workflows | `hve-core-all`, `developer` | -| RPI workflow prompts | `hve-core-all` plus all relevant collections | -| Documentation workflows | `hve-core-all`, `technical-writer` | -| Architecture prompts | `hve-core-all`, `architect` | - -### Tags for Prompts - -Common tags for prompts: - -| Tag | Use For | -|----------------------|----------------------------------| -| `rpi` | Research-Plan-Implement workflow | -| `git` | Git operations | -| `github` | GitHub-specific workflows | -| `ado` | Azure DevOps workflows | -| `planning` | Planning and estimation | -| `implementation` | Code implementation | -| `review` | Review processes | -| `documentation` | Documentation generation | -| `prompt-engineering` | Prompt building and analysis | +| Git/PR workflows | `hve-core-all`, `git` | +| ADO work item workflows | `hve-core-all`, `ado`, `project-planning` | +| GitHub issue workflows | `hve-core-all`, `github` | +| RPI workflow prompts | `hve-core-all`, `rpi` | +| Documentation workflows | `hve-core-all`, `prompt-engineering` | +| Architecture prompts | `hve-core-all`, `project-planning` | For complete collection documentation, see [AI Artifacts Common Standards - Collection Manifests](ai-artifacts-common.md#collection-manifests). diff --git a/docs/contributing/skills.md b/docs/contributing/skills.md index 2857af38..a581234b 100644 --- a/docs/contributing/skills.md +++ b/docs/contributing/skills.md @@ -109,24 +109,13 @@ items: Choose collections based on who uses the skill's utilities: -| Skill Type | Recommended Collections | -|----------------------|---------------------------------------| -| Media processing | `hve-core-all` | -| Documentation tools | `hve-core-all`, `technical-writer` | -| Data processing | `hve-core-all`, `developer` | -| Infrastructure tools | `hve-core-all`, `devops`, `architect` | -| Code generation | `hve-core-all`, `developer` | - -### Tags for Skills - -Common tags for skills: - -| Tag | Use For | -|-----------|--------------------------------| -| `media` | Image, video, audio processing | -| `tooling` | General development utilities | -| `data` | Data transformation | -| `testing` | Test automation utilities | +| Skill Type | Recommended Collections | +|----------------------|--------------------------------------------------| +| Media processing | `hve-core-all` | +| Documentation tools | `hve-core-all`, `prompt-engineering` | +| Data processing | `hve-core-all`, `data-science` | +| Infrastructure tools | `hve-core-all`, `coding-standards` | +| Code generation | `hve-core-all`, `coding-standards` | For complete collection documentation, see [AI Artifacts Common Standards - Collection Manifests](ai-artifacts-common.md#collection-manifests). From fc18595668f3f3e348fe07832be1b94f34d29fd5 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 13 Feb 2026 11:38:03 +0100 Subject: [PATCH 60/62] test(scripts): add coverage for Prepare-Extension and Generate-Plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create Generate-Plugins.Tests.ps1 with 21 tests for channel filtering and plugin orchestration - add 30+ tests to Prepare-Extension.Tests.ps1 for uncovered error and edge-case paths - include plugins directory in pester.config.ps1 coverage tracking ๐Ÿงช - Generated by Copilot --- .../extension/Prepare-Extension.Tests.ps1 | 564 ++++++++++++++++++ scripts/tests/pester.config.ps1 | 2 +- .../tests/plugins/Generate-Plugins.Tests.ps1 | 322 ++++++++++ 3 files changed, 887 insertions(+), 1 deletion(-) create mode 100644 scripts/tests/plugins/Generate-Plugins.Tests.ps1 diff --git a/scripts/tests/extension/Prepare-Extension.Tests.ps1 b/scripts/tests/extension/Prepare-Extension.Tests.ps1 index f9e1f34c..29849675 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -1537,3 +1537,567 @@ maturity: experimental } } } + +#region Additional Coverage Tests + +Describe 'Get-ArtifactDescription' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Returns empty string when file does not exist' { + $result = Get-ArtifactDescription -FilePath (Join-Path $script:tempDir 'nonexistent.md') + $result | Should -Be '' + } + + It 'Returns empty string when file has no frontmatter' { + $path = Join-Path $script:tempDir 'no-frontmatter.md' + '# Just a heading' | Set-Content -Path $path + $result = Get-ArtifactDescription -FilePath $path + $result | Should -Be '' + } + + It 'Returns empty string when frontmatter has no description' { + $path = Join-Path $script:tempDir 'no-desc.md' + @" +--- +applyTo: "**/*.ps1" +--- +# No description +"@ | Set-Content -Path $path + $result = Get-ArtifactDescription -FilePath $path + $result | Should -Be '' + } + + It 'Returns description from valid frontmatter' { + $path = Join-Path $script:tempDir 'valid.md' + @" +--- +description: "My artifact description" +--- +# Valid +"@ | Set-Content -Path $path + $result = Get-ArtifactDescription -FilePath $path + $result | Should -Be 'My artifact description' + } + + It 'Strips branding suffix from description' { + $path = Join-Path $script:tempDir 'branded.md' + @" +--- +description: "Some tool - Brought to you by microsoft/hve-core" +--- +# Branded +"@ | Set-Content -Path $path + $result = Get-ArtifactDescription -FilePath $path + $result | Should -Be 'Some tool' + } + + It 'Returns empty string when frontmatter YAML is invalid' { + $path = Join-Path $script:tempDir 'bad-yaml.md' + @" +--- +description: [invalid: yaml: : +--- +# Bad +"@ | Set-Content -Path $path + $result = Get-ArtifactDescription -FilePath $path + $result | Should -Be '' + } +} + +Describe 'Get-CollectionArtifactKey - default branch' { + It 'Handles unknown kind with matching suffix' { + $result = Get-CollectionArtifactKey -Kind 'custom' -Path '.github/custom/my-file.custom.md' + $result | Should -Be 'my-file' + } + + It 'Handles unknown kind with .md extension but no matching suffix' { + $result = Get-CollectionArtifactKey -Kind 'custom' -Path '.github/custom/readme.md' + $result | Should -Be 'readme' + } + + It 'Handles unknown kind with non-md file' { + $result = Get-CollectionArtifactKey -Kind 'custom' -Path '.github/custom/config.json' + $result | Should -Be 'config.json' + } +} + +Describe 'Test-TemplateConsistency' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Returns inconsistent when template file not found' { + $manifest = @{ name = 'test'; displayName = 'Test'; description = 'Desc' } + $result = Test-TemplateConsistency -TemplatePath (Join-Path $script:tempDir 'nonexistent.json') -CollectionManifest $manifest + $result.IsConsistent | Should -BeFalse + $result.Mismatches.Count | Should -Be 1 + $result.Mismatches[0].Field | Should -Be 'file' + $result.Mismatches[0].Message | Should -Match 'not found' + } + + It 'Returns inconsistent when template is invalid JSON' { + $badPath = Join-Path $script:tempDir 'bad-template.json' + 'not valid json {{{' | Set-Content -Path $badPath + $manifest = @{ name = 'test' } + $result = Test-TemplateConsistency -TemplatePath $badPath -CollectionManifest $manifest + $result.IsConsistent | Should -BeFalse + $result.Mismatches[0].Message | Should -Match 'Failed to parse' + } + + It 'Returns consistent when fields match' { + $path = Join-Path $script:tempDir 'matching.json' + @{ name = 'hve-rpi'; displayName = 'HVE RPI'; description = 'RPI tools' } | ConvertTo-Json | Set-Content -Path $path + $manifest = @{ name = 'hve-rpi'; displayName = 'HVE RPI'; description = 'RPI tools' } + $result = Test-TemplateConsistency -TemplatePath $path -CollectionManifest $manifest + $result.IsConsistent | Should -BeTrue + $result.Mismatches.Count | Should -Be 0 + } + + It 'Reports mismatches for diverging fields' { + $path = Join-Path $script:tempDir 'diverging.json' + @{ name = 'old-name'; displayName = 'Old Name'; description = 'Old desc' } | ConvertTo-Json | Set-Content -Path $path + $manifest = @{ name = 'new-name'; displayName = 'New Name'; description = 'New desc' } + $result = Test-TemplateConsistency -TemplatePath $path -CollectionManifest $manifest + $result.IsConsistent | Should -BeFalse + $result.Mismatches.Count | Should -Be 3 + } + + It 'Skips comparison when field missing in either side' { + $path = Join-Path $script:tempDir 'partial.json' + @{ name = 'test' } | ConvertTo-Json | Set-Content -Path $path + $manifest = @{ displayName = 'Test Display' } + $result = Test-TemplateConsistency -TemplatePath $path -CollectionManifest $manifest + $result.IsConsistent | Should -BeTrue + } +} + +Describe 'Update-PackageJsonContributes - existing contributes fields' { + It 'Updates existing chatAgents field via else branch' { + $packageJson = [PSCustomObject]@{ + name = 'test-extension' + contributes = [PSCustomObject]@{ + chatAgents = @(@{ path = './old.agent.md' }) + chatPromptFiles = @(@{ path = './old.prompt.md' }) + chatInstructions = @(@{ path = './old.instr.md' }) + chatSkills = @(@{ path = './old.skill' }) + } + } + $agents = @(@{ name = 'new-agent'; path = './.github/agents/new.agent.md' }) + $prompts = @(@{ name = 'new-prompt'; path = './.github/prompts/new.prompt.md' }) + $instructions = @(@{ name = 'new-instr'; path = './.github/instructions/new.instructions.md' }) + $skills = @(@{ name = 'new-skill'; path = './.github/skills/new-skill' }) + + $result = Update-PackageJsonContributes -PackageJson $packageJson ` + -ChatAgents $agents ` + -ChatPromptFiles $prompts ` + -ChatInstructions $instructions ` + -ChatSkills $skills + + $result.contributes.chatAgents[0].path | Should -Be './.github/agents/new.agent.md' + $result.contributes.chatPromptFiles[0].path | Should -Be './.github/prompts/new.prompt.md' + $result.contributes.chatInstructions[0].path | Should -Be './.github/instructions/new.instructions.md' + $result.contributes.chatSkills[0].path | Should -Be './.github/skills/new-skill' + } + + It 'Adds contributes section when missing' { + $packageJson = [PSCustomObject]@{ + name = 'bare-extension' + } + + $result = Update-PackageJsonContributes -PackageJson $packageJson ` + -ChatAgents @() ` + -ChatPromptFiles @() ` + -ChatInstructions @() ` + -ChatSkills @() + + $result.contributes | Should -Not -BeNullOrEmpty + } +} + +Describe 'Resolve-HandoffDependencies - additional cases' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + $script:agentsDir = Join-Path $script:tempDir 'agents' + New-Item -ItemType Directory -Path $script:agentsDir -Force | Out-Null + + # Agent with string-format handoffs + @' +--- +description: "String handoff agent" +handoffs: + - string-target +--- +'@ | Set-Content -Path (Join-Path $script:agentsDir 'string-handoff.agent.md') + + @' +--- +description: "String target" +--- +'@ | Set-Content -Path (Join-Path $script:agentsDir 'string-target.agent.md') + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Resolves string-format handoffs' { + $result = Resolve-HandoffDependencies -SeedAgents @('string-handoff') -AgentsDir $script:agentsDir + $result | Should -Contain 'string-handoff' + $result | Should -Contain 'string-target' + } + + It 'Warns but continues when handoff target file is missing' { + $result = Resolve-HandoffDependencies -SeedAgents @('missing-agent') -AgentsDir $script:agentsDir 3>&1 + # The function emits a warning and returns the seed agent + $agentNames = @($result | Where-Object { $_ -is [string] }) + $agentNames | Should -Contain 'missing-agent' + } +} + +Describe 'Get-DiscoveredPrompts - maturity filtering' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + $script:promptsDir = Join-Path $script:tempDir 'prompts' + $script:ghDir = Join-Path $script:tempDir '.github' + New-Item -ItemType Directory -Path $script:promptsDir -Force | Out-Null + New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null + + @' +--- +description: "Stable prompt" +--- +'@ | Set-Content -Path (Join-Path $script:promptsDir 'stable.prompt.md') + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Skips prompts when none match allowed maturities' { + $result = Get-DiscoveredPrompts -PromptsDir $script:promptsDir -GitHubDir $script:ghDir -AllowedMaturities @('experimental') + $result.Prompts.Count | Should -Be 0 + $result.Skipped.Count | Should -Be 1 + } +} + +Describe 'Get-DiscoveredInstructions - maturity filtering' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + $script:instrDir = Join-Path $script:tempDir 'instructions' + $script:ghDir = Join-Path $script:tempDir '.github' + New-Item -ItemType Directory -Path $script:instrDir -Force | Out-Null + New-Item -ItemType Directory -Path $script:ghDir -Force | Out-Null + + @' +--- +description: "Test instruction" +applyTo: "**/*.ps1" +--- +'@ | Set-Content -Path (Join-Path $script:instrDir 'test.instructions.md') + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Skips instructions when none match allowed maturities' { + $result = Get-DiscoveredInstructions -InstructionsDir $script:instrDir -GitHubDir $script:ghDir -AllowedMaturities @('experimental') + $result.Instructions.Count | Should -Be 0 + $result.Skipped.Count | Should -Be 1 + } +} + +Describe 'Invoke-PrepareExtension - error cases' { + BeforeAll { + $script:tempDir = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null + + $script:extDir = Join-Path $script:tempDir 'extension' + New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null + + $script:templatesDir = Join-Path $script:extDir 'templates' + New-Item -ItemType Directory -Path $script:templatesDir -Force | Out-Null + @' +{ + "name": "hve-core", + "displayName": "HVE Core", + "version": "1.0.0", + "description": "Test extension", + "publisher": "test-pub", + "engines": { "vscode": "^1.80.0" }, + "contributes": {} +} +'@ | Set-Content -Path (Join-Path $script:templatesDir 'package.template.json') + + $script:collectionsDir = Join-Path $script:tempDir 'collections' + New-Item -ItemType Directory -Path $script:collectionsDir -Force | Out-Null + @" +id: hve-core-all +name: hve-core +displayName: HVE Core +description: Test +"@ | Set-Content -Path (Join-Path $script:collectionsDir 'hve-core-all.collection.yml') + + $script:ghDir = Join-Path $script:tempDir '.github' + New-Item -ItemType Directory -Path (Join-Path $script:ghDir 'agents') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $script:ghDir 'prompts') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $script:ghDir 'instructions') -Force | Out-Null + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Fails when package.json has invalid JSON' { + # Write invalid JSON and mock generation to preserve it + $badPkgPath = Join-Path $script:extDir 'package.json' + 'NOT VALID JSON' | Set-Content -Path $badPkgPath + + Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) } + + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' + + $result.Success | Should -BeFalse + $result.ErrorMessage | Should -Match 'Failed to parse package.json' + } + + It 'Fails when package.json lacks version field' { + $badPkgPath = Join-Path $script:extDir 'package.json' + @{ name = 'test-no-version' } | ConvertTo-Json | Set-Content -Path $badPkgPath + + Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) } + + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' + + $result.Success | Should -BeFalse + $result.ErrorMessage | Should -Match "does not contain a 'version' field" + } + + It 'Fails when version format is invalid' { + $badPkgPath = Join-Path $script:extDir 'package.json' + @{ name = 'test'; version = 'not-semver' } | ConvertTo-Json | Set-Content -Path $badPkgPath + + Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) } + + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' + + $result.Success | Should -BeFalse + $result.ErrorMessage | Should -Match 'Invalid version format' + } + + It 'Warns when changelog path specified but file not found' { + $validPkgPath = Join-Path $script:extDir 'package.json' + @{ name = 'test'; version = '1.0.0'; contributes = @{} } | ConvertTo-Json -Depth 5 | Set-Content -Path $validPkgPath + + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' ` + -ChangelogPath (Join-Path $script:tempDir 'NONEXISTENT-CHANGELOG.md') 3>&1 + + # Filter out the result hashtable from warnings + $hashtableResult = $result | Where-Object { $_ -is [hashtable] } + if ($hashtableResult) { + $hashtableResult.Success | Should -BeTrue + } + } + + Context 'Collection with requires dependencies' { + BeforeAll { + $script:reqCollectionPath = Join-Path $script:tempDir 'requires-test.collection.yml' + @" +id: hve-core-all +name: hve-core-all +displayName: HVE Core All +description: Requires test +items: + - kind: agent + path: .github/agents/main.agent.md + maturity: stable + requires: + prompts: + - dep-prompt + - kind: prompt + path: .github/prompts/dep-prompt.prompt.md + maturity: stable +"@ | Set-Content -Path $script:reqCollectionPath + + # Create required agent and prompt files + @' +--- +description: "Main agent" +--- +'@ | Set-Content -Path (Join-Path $script:ghDir 'agents/main.agent.md') + + @' +--- +description: "Dependent prompt" +--- +'@ | Set-Content -Path (Join-Path $script:ghDir 'prompts/dep-prompt.prompt.md') + + # Restore valid package.json + $validPkgPath = Join-Path $script:extDir 'package.json' + @{ name = 'hve-core'; version = '1.0.0'; contributes = @{} } | ConvertTo-Json -Depth 5 | Set-Content -Path $validPkgPath + } + + It 'Resolves requires dependencies in collection' { + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' ` + -Collection $script:reqCollectionPath ` + -DryRun + + $result.Success | Should -BeTrue + $result.AgentCount | Should -BeGreaterOrEqual 1 + $result.PromptCount | Should -BeGreaterOrEqual 1 + } + } +} + +Describe 'Invoke-ExtensionCollectionsGeneration - collection manifest errors' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + + $collectionsDir = Join-Path $script:tempDir 'collections' + $templatesDir = Join-Path $script:tempDir 'extension/templates' + New-Item -ItemType Directory -Path $collectionsDir -Force | Out-Null + New-Item -ItemType Directory -Path $templatesDir -Force | Out-Null + + @{ + name = 'hve-core' + displayName = 'HVE Core' + version = '1.0.0' + description = 'default' + publisher = 'test-pub' + engines = @{ vscode = '^1.80.0' } + contributes = @{} + } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $templatesDir 'package.template.json') + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Throws when collection id is empty' { + $collectionsDir = Join-Path $script:tempDir 'collections' + Remove-Item -Path "$collectionsDir/*" -Force -ErrorAction SilentlyContinue + @" +id: +name: empty-id +"@ | Set-Content -Path (Join-Path $collectionsDir 'empty.collection.yml') + + { Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir } | Should -Throw '*Collection id is required*' + } + + It 'Throws when collection manifest is not a hashtable' { + $collectionsDir = Join-Path $script:tempDir 'collections' + Remove-Item -Path "$collectionsDir/*" -Force -ErrorAction SilentlyContinue + # YAML that parses as a scalar string + 'just a string' | Set-Content -Path (Join-Path $collectionsDir 'bad.collection.yml') + + { Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir } | Should -Throw '*must be a hashtable*' + } +} + +Describe 'Invoke-ExtensionCollectionsGeneration - README generation' { + BeforeAll { + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + + $collectionsDir = Join-Path $script:tempDir 'collections' + $templatesDir = Join-Path $script:tempDir 'extension/templates' + New-Item -ItemType Directory -Path $collectionsDir -Force | Out-Null + New-Item -ItemType Directory -Path $templatesDir -Force | Out-Null + + # Package template + @{ + name = 'hve-core' + displayName = 'HVE Core' + version = '1.0.0' + description = 'default' + publisher = 'test-pub' + engines = @{ vscode = '^1.80.0' } + contributes = @{} + } | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $templatesDir 'package.template.json') + + # README template + $repoRoot = (Get-Item "$PSScriptRoot/../../..").FullName + $realTemplatePath = Join-Path $repoRoot 'extension/templates/README.template.md' + if (Test-Path $realTemplatePath) { + Copy-Item -Path $realTemplatePath -Destination (Join-Path $templatesDir 'README.template.md') + } + else { + @" +# {{DISPLAY_NAME}} + +> {{DESCRIPTION}} + +{{BODY}} + +{{ARTIFACTS}} + +{{FULL_EDITION}} +"@ | Set-Content -Path (Join-Path $templatesDir 'README.template.md') + } + + # Collection with a .collection.md body file + @" +id: readme-test +name: README Test +displayName: HVE Core - README Test +description: Test readme generation +"@ | Set-Content -Path (Join-Path $collectionsDir 'readme-test.collection.yml') + + 'Body content for readme test.' | Set-Content -Path (Join-Path $collectionsDir 'readme-test.collection.md') + + # hve-core-all needed for the defaults + @" +id: hve-core-all +name: hve-core +displayName: HVE Core +description: All artifacts +"@ | Set-Content -Path (Join-Path $collectionsDir 'hve-core-all.collection.yml') + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Generates README files for collections with .collection.md' { + $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir + $readmePath = Join-Path $script:tempDir 'extension/README.readme-test.md' + Test-Path $readmePath | Should -BeTrue + $content = Get-Content -Path $readmePath -Raw + $content | Should -Match 'Body content for readme test' + } + + It 'Skips README generation when .collection.md is missing' { + $null = Invoke-ExtensionCollectionsGeneration -RepoRoot $script:tempDir + # hve-core-all has no .md body in this test setup + $readmePath = Join-Path $script:tempDir 'extension/README.md' + Test-Path $readmePath | Should -BeFalse + } +} + +#endregion Additional Coverage Tests diff --git a/scripts/tests/pester.config.ps1 b/scripts/tests/pester.config.ps1 index 2f6bc823..fc9f6c35 100644 --- a/scripts/tests/pester.config.ps1 +++ b/scripts/tests/pester.config.ps1 @@ -50,7 +50,7 @@ if ($CodeCoverage.IsPresent) { # Resolve coverage paths explicitly - Join-Path with wildcards returns literal paths without file system expansion in Pester configuration $scriptRoot = Split-Path $PSScriptRoot -Parent - $coverageDirs = @('linting', 'security', 'dev-tools', 'lib', 'extension') + $coverageDirs = @('linting', 'security', 'dev-tools', 'lib', 'extension', 'plugins') $coveragePaths = $coverageDirs | ForEach-Object { Get-ChildItem -Path (Join-Path $scriptRoot $_) -Include '*.ps1', '*.psm1' -Recurse -File -ErrorAction SilentlyContinue diff --git a/scripts/tests/plugins/Generate-Plugins.Tests.ps1 b/scripts/tests/plugins/Generate-Plugins.Tests.ps1 new file mode 100644 index 00000000..024a77cd --- /dev/null +++ b/scripts/tests/plugins/Generate-Plugins.Tests.ps1 @@ -0,0 +1,322 @@ +#Requires -Modules Pester +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: MIT + +BeforeAll { + . $PSScriptRoot/../../plugins/Generate-Plugins.ps1 +} + +Describe 'Get-AllowedCollectionMaturities' { + It 'Returns only stable for Stable channel' { + $result = Get-AllowedCollectionMaturities -Channel 'Stable' + $result | Should -Be @('stable') + } + + It 'Returns stable, preview, and experimental for PreRelease channel' { + $result = Get-AllowedCollectionMaturities -Channel 'PreRelease' + $result | Should -Contain 'stable' + $result | Should -Contain 'preview' + $result | Should -Contain 'experimental' + } + + It 'Does not include deprecated for either channel' { + $stable = Get-AllowedCollectionMaturities -Channel 'Stable' + $preRelease = Get-AllowedCollectionMaturities -Channel 'PreRelease' + $stable | Should -Not -Contain 'deprecated' + $preRelease | Should -Not -Contain 'deprecated' + } +} + +Describe 'Select-CollectionItemsByChannel' { + It 'Includes stable items on Stable channel' { + $collection = @{ + id = 'test' + items = @( + @{ kind = 'agent'; path = '.github/agents/a.agent.md'; maturity = 'stable' } + ) + } + $result = Select-CollectionItemsByChannel -Collection $collection -Channel 'Stable' + $result.items.Count | Should -Be 1 + } + + It 'Excludes preview items on Stable channel' { + $collection = @{ + id = 'test' + items = @( + @{ kind = 'agent'; path = '.github/agents/a.agent.md'; maturity = 'stable' }, + @{ kind = 'agent'; path = '.github/agents/b.agent.md'; maturity = 'preview' } + ) + } + $result = Select-CollectionItemsByChannel -Collection $collection -Channel 'Stable' + $result.items.Count | Should -Be 1 + } + + It 'Includes preview and experimental items on PreRelease channel' { + $collection = @{ + id = 'test' + items = @( + @{ kind = 'agent'; path = '.github/agents/a.agent.md'; maturity = 'stable' }, + @{ kind = 'prompt'; path = '.github/prompts/b.prompt.md'; maturity = 'preview' }, + @{ kind = 'instruction'; path = '.github/instructions/c.instructions.md'; maturity = 'experimental' } + ) + } + $result = Select-CollectionItemsByChannel -Collection $collection -Channel 'PreRelease' + $result.items.Count | Should -Be 3 + } + + It 'Excludes deprecated items on PreRelease channel' { + $collection = @{ + id = 'test' + items = @( + @{ kind = 'agent'; path = '.github/agents/a.agent.md'; maturity = 'stable' }, + @{ kind = 'agent'; path = '.github/agents/old.agent.md'; maturity = 'deprecated' } + ) + } + $result = Select-CollectionItemsByChannel -Collection $collection -Channel 'PreRelease' + $result.items.Count | Should -Be 1 + } + + It 'Defaults to stable when maturity is null' { + $collection = @{ + id = 'test' + items = @( + @{ kind = 'agent'; path = '.github/agents/a.agent.md'; maturity = $null } + ) + } + $result = Select-CollectionItemsByChannel -Collection $collection -Channel 'Stable' + $result.items.Count | Should -Be 1 + } + + It 'Preserves non-items keys from collection' { + $collection = @{ + id = 'test' + name = 'Test Collection' + description = 'desc' + items = @( + @{ kind = 'agent'; path = '.github/agents/a.agent.md'; maturity = 'stable' } + ) + } + $result = Select-CollectionItemsByChannel -Collection $collection -Channel 'Stable' + $result.id | Should -Be 'test' + $result.name | Should -Be 'Test Collection' + $result.description | Should -Be 'desc' + } +} + +Describe 'Invoke-PluginGeneration' { + BeforeAll { + $script:tempDir = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null + + # Create package.json + @{ + name = 'hve-core' + version = '1.0.0' + description = 'test' + author = 'test-author' + } | ConvertTo-Json | Set-Content -Path (Join-Path $script:tempDir 'package.json') + + # Create collections directory with manifests + $collectionsDir = Join-Path $script:tempDir 'collections' + New-Item -ItemType Directory -Path $collectionsDir -Force | Out-Null + + # Create .github structure with artifacts + $ghDir = Join-Path $script:tempDir '.github' + $agentsDir = Join-Path $ghDir 'agents' + $promptsDir = Join-Path $ghDir 'prompts' + $instrDir = Join-Path $ghDir 'instructions' + $skillsDir = Join-Path $ghDir 'skills/test-skill' + New-Item -ItemType Directory -Path $agentsDir -Force | Out-Null + New-Item -ItemType Directory -Path $promptsDir -Force | Out-Null + New-Item -ItemType Directory -Path $instrDir -Force | Out-Null + New-Item -ItemType Directory -Path $skillsDir -Force | Out-Null + + @' +--- +description: "Test agent" +--- +'@ | Set-Content -Path (Join-Path $agentsDir 'test.agent.md') + + @' +--- +description: "Test prompt" +--- +'@ | Set-Content -Path (Join-Path $promptsDir 'test.prompt.md') + + @' +--- +description: "Test instruction" +applyTo: "**/*.ps1" +--- +'@ | Set-Content -Path (Join-Path $instrDir 'test.instructions.md') + + @' +--- +name: test-skill +description: "Test skill" +--- +'@ | Set-Content -Path (Join-Path $skillsDir 'SKILL.md') + + # Create docs/templates and scripts directories for shared symlinking + New-Item -ItemType Directory -Path (Join-Path $script:tempDir 'docs/templates') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $script:tempDir 'scripts/dev-tools') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $script:tempDir 'scripts/lib') -Force | Out-Null + + # Create plugins directory + New-Item -ItemType Directory -Path (Join-Path $script:tempDir 'plugins') -Force | Out-Null + + # Create .github/plugin directory for marketplace manifest + New-Item -ItemType Directory -Path (Join-Path $script:tempDir '.github/plugin') -Force | Out-Null + + # hve-core-all collection + @" +id: hve-core-all +name: hve-core +description: All artifacts +tags: + - copilot +items: + - path: .github/agents/test.agent.md + kind: agent + - path: .github/prompts/test.prompt.md + kind: prompt + - path: .github/instructions/test.instructions.md + kind: instruction + - path: .github/skills/test-skill + kind: skill +display: + color: blue +"@ | Set-Content -Path (Join-Path $collectionsDir 'hve-core-all.collection.yml') + } + + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Generates plugins successfully' { + $result = Invoke-PluginGeneration -RepoRoot $script:tempDir -Refresh -Channel 'PreRelease' + $result.Success | Should -BeTrue + $result.PluginCount | Should -BeGreaterOrEqual 1 + } + + It 'Creates plugin directory' { + $pluginDir = Join-Path $script:tempDir 'plugins/hve-core-all' + Test-Path $pluginDir | Should -BeTrue + } + + It 'Generates plugin.json manifest' { + $manifestPath = Join-Path $script:tempDir 'plugins/hve-core-all/.github/plugin/plugin.json' + Test-Path $manifestPath | Should -BeTrue + $manifest = Get-Content -Path $manifestPath -Raw | ConvertFrom-Json + $manifest.name | Should -Be 'hve-core-all' + } + + It 'Generates README.md' { + $readmePath = Join-Path $script:tempDir 'plugins/hve-core-all/README.md' + Test-Path $readmePath | Should -BeTrue + } + + It 'Filters to specific collection IDs when provided' { + $result = Invoke-PluginGeneration -RepoRoot $script:tempDir -CollectionIds @('hve-core-all') -Refresh -Channel 'PreRelease' + $result.PluginCount | Should -Be 1 + } + + It 'Warns for non-existent collection IDs' { + $result = Invoke-PluginGeneration -RepoRoot $script:tempDir -CollectionIds @('nonexistent') -Refresh -Channel 'PreRelease' 3>&1 + $warnings = @($result | Where-Object { $_ -is [System.Management.Automation.WarningRecord] }) + $warnings.Count | Should -BeGreaterOrEqual 1 + } + + It 'Supports DryRun mode' { + $dryRunDir = Join-Path $script:tempDir 'plugins/dryrun-test' + $result = Invoke-PluginGeneration -RepoRoot $script:tempDir -CollectionIds @('hve-core-all') -DryRun -Channel 'PreRelease' + $result.Success | Should -BeTrue + } + + It 'Returns zero plugins when no collections found' { + $emptyRoot = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'collections') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'plugins') -Force | Out-Null + @{ name = 'test'; version = '1.0.0'; description = 'test'; author = 'test' } | + ConvertTo-Json | Set-Content -Path (Join-Path $emptyRoot 'package.json') + + # Create minimal .github structure for auto-update + New-Item -ItemType Directory -Path (Join-Path $emptyRoot '.github/agents') -Force | Out-Null + @" +id: hve-core-all +name: hve-core +description: test +tags: [] +items: [] +display: {} +"@ | Set-Content -Path (Join-Path $emptyRoot 'collections/hve-core-all.collection.yml') + + $result = Invoke-PluginGeneration -RepoRoot $emptyRoot -CollectionIds @('missing-id') -Channel 'PreRelease' 3>&1 + $hashtableResult = $result | Where-Object { $_ -is [hashtable] } + if ($hashtableResult) { + $hashtableResult.PluginCount | Should -Be 0 + } + } + + It 'Applies channel filtering to items' { + # Add a collection with mixed maturities + $mixedPath = Join-Path (Join-Path $script:tempDir 'collections') 'mixed.collection.yml' + @" +id: mixed +name: Mixed Collection +description: Mixed maturity test +items: + - path: .github/agents/test.agent.md + kind: agent + maturity: stable + - path: .github/prompts/test.prompt.md + kind: prompt + maturity: experimental +"@ | Set-Content -Path $mixedPath + + $result = Invoke-PluginGeneration -RepoRoot $script:tempDir -CollectionIds @('mixed') -Refresh -Channel 'Stable' + $result.Success | Should -BeTrue + } + + It 'Removes existing plugin directory on Refresh' { + # Create a stale file in plugin dir + $staleDir = Join-Path $script:tempDir 'plugins/hve-core-all/stale' + New-Item -ItemType Directory -Path $staleDir -Force | Out-Null + 'stale' | Set-Content -Path (Join-Path $staleDir 'file.txt') + + $result = Invoke-PluginGeneration -RepoRoot $script:tempDir -CollectionIds @('hve-core-all') -Refresh -Channel 'PreRelease' + $result.Success | Should -BeTrue + Test-Path $staleDir | Should -BeFalse + } + + It 'Logs DryRun message when refreshing existing plugin' { + # Ensure plugin directory exists + $pluginDir = Join-Path $script:tempDir 'plugins/hve-core-all' + New-Item -ItemType Directory -Path $pluginDir -Force | Out-Null + + $output = Invoke-PluginGeneration -RepoRoot $script:tempDir ` + -CollectionIds @('hve-core-all') ` + -Refresh -DryRun -Channel 'PreRelease' 6>&1 + + $dryRunMessages = @($output | Where-Object { "$_" -match 'DRY RUN.*Would remove' }) + $dryRunMessages.Count | Should -BeGreaterOrEqual 1 + } + + It 'Warns when collections directory has no matching YAML files' { + $emptyRoot = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString()) + $emptyCollDir = Join-Path $emptyRoot 'collections' + New-Item -ItemType Directory -Path $emptyCollDir -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $emptyRoot 'plugins') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $emptyRoot '.github/agents') -Force | Out-Null + @{ name = 'test'; version = '1.0.0'; description = 'test'; author = 'test' } | + ConvertTo-Json | Set-Content -Path (Join-Path $emptyRoot 'package.json') + + # Mock Update-HveCoreAllCollection to avoid file-not-found errors + Mock Update-HveCoreAllCollection { return @{ ItemCount = 0; AddedCount = 0; RemovedCount = 0 } } + + $result = Invoke-PluginGeneration -RepoRoot $emptyRoot -Channel 'PreRelease' 3>&1 + $warnings = @($result | Where-Object { $_ -is [System.Management.Automation.WarningRecord] }) + $warnings.Count | Should -BeGreaterOrEqual 1 + $warnings[0].Message | Should -Match 'No collection manifests found' + } +} From 31efed5e06b86b3c5752c6a6fce7a288a2f7b45d Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 13 Feb 2026 11:53:23 +0100 Subject: [PATCH 61/62] docs: linting tables format --- docs/contributing/ai-artifacts-common.md | 46 ++++++++++++------------ docs/contributing/custom-agents.md | 16 ++++----- docs/contributing/prompts.md | 16 ++++----- docs/contributing/skills.md | 14 ++++---- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 08cefd3d..da5ef48b 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -122,18 +122,18 @@ items: Each collection manifest declares a top-level `tags` array for categorization and discoverability. Tags exist **only at the collection level**, not on individual items. -| Collection | Tags | -|----------------------|-------------------------------------------------------------------| -| `hve-core-all` | `hve`, `complete`, `bundle` | -| `ado` | `azure-devops`, `ado`, `work-items`, `builds`, `pull-requests` | -| `coding-standards` | `coding-standards`, `bash`, `bicep`, `csharp`, `python`, `terraform`, `uv` | +| Collection | Tags | +|----------------------|-------------------------------------------------------------------------------| +| `hve-core-all` | `hve`, `complete`, `bundle` | +| `ado` | `azure-devops`, `ado`, `work-items`, `builds`, `pull-requests` | +| `coding-standards` | `coding-standards`, `bash`, `bicep`, `csharp`, `python`, `terraform`, `uv` | | `data-science` | `data`, `jupyter`, `streamlit`, `dashboards`, `visualization`, `data-science` | -| `git` | `git`, `commits`, `merge`, `pull-request` | -| `github` | `github`, `issues`, `backlog`, `triage`, `sprint` | -| `project-planning` | `documentation`, `architecture`, `adr`, `brd`, `prd`, `diagrams`, `planning` | -| `prompt-engineering` | `prompts`, `agents`, `authoring`, `refactoring` | -| `rpi` | `workflow`, `rpi`, `planning`, `research`, `implementation`, `review` | -| `security-planning` | `security`, `incident-response`, `risk`, `planning` | +| `git` | `git`, `commits`, `merge`, `pull-request` | +| `github` | `github`, `issues`, `backlog`, `triage`, `sprint` | +| `project-planning` | `documentation`, `architecture`, `adr`, `brd`, `prd`, `diagrams`, `planning` | +| `prompt-engineering` | `prompts`, `agents`, `authoring`, `refactoring` | +| `rpi` | `workflow`, `rpi`, `planning`, `research`, `implementation`, `review` | +| `security-planning` | `security`, `incident-response`, `risk`, `planning` | When creating a new collection, choose tags that describe the domain, technologies, and workflows covered. Use lowercase kebab-case and prefer existing tags before introducing new ones. @@ -180,18 +180,18 @@ Collections represent role-targeted artifact packages for HVE-Core artifacts. Th ### Defined Collections -| Collection | Identifier | Description | -|--------------------------|----------------------|--------------------------------------------------------------------------------| -| **All** | `hve-core-all` | Full bundle of all stable HVE Core agents, prompts, instructions, and skills | -| **Azure DevOps** | `ado` | Azure DevOps work item management, build monitoring, and pull request creation | -| **Coding Standards** | `coding-standards` | Language-specific coding instructions for bash, Bicep, C#, Python, and Terraform | -| **Data Science** | `data-science` | Data specification generation, Jupyter notebooks, and Streamlit dashboards | -| **Git Workflow** | `git` | Git commit messages, merges, setup, and pull request prompts | -| **GitHub Backlog** | `github` | GitHub issue discovery, triage, sprint planning, and backlog execution | -| **Project Planning** | `project-planning` | PRDs, BRDs, ADRs, architecture diagrams, and documentation operations | -| **Prompt Engineering** | `prompt-engineering` | Tools for analyzing, building, and refactoring prompts, agents, and instructions | -| **RPI Workflow** | `rpi` | Research, Plan, Implement, Review workflow agents and prompts | -| **Security Planning** | `security-planning` | Security plan creation, incident response, and risk assessment | +| Collection | Identifier | Description | +|------------------------|----------------------|----------------------------------------------------------------------------------| +| **All** | `hve-core-all` | Full bundle of all stable HVE Core agents, prompts, instructions, and skills | +| **Azure DevOps** | `ado` | Azure DevOps work item management, build monitoring, and pull request creation | +| **Coding Standards** | `coding-standards` | Language-specific coding instructions for bash, Bicep, C#, Python, and Terraform | +| **Data Science** | `data-science` | Data specification generation, Jupyter notebooks, and Streamlit dashboards | +| **Git Workflow** | `git` | Git commit messages, merges, setup, and pull request prompts | +| **GitHub Backlog** | `github` | GitHub issue discovery, triage, sprint planning, and backlog execution | +| **Project Planning** | `project-planning` | PRDs, BRDs, ADRs, architecture diagrams, and documentation operations | +| **Prompt Engineering** | `prompt-engineering` | Tools for analyzing, building, and refactoring prompts, agents, and instructions | +| **RPI Workflow** | `rpi` | Research, Plan, Implement, Review workflow agents and prompts | +| **Security Planning** | `security-planning` | Security plan creation, incident response, and risk assessment | ### Collection Assignment Guidelines diff --git a/docs/contributing/custom-agents.md b/docs/contributing/custom-agents.md index 5d5b6c6a..da69fbb7 100644 --- a/docs/contributing/custom-agents.md +++ b/docs/contributing/custom-agents.md @@ -219,14 +219,14 @@ items: Choose collections based on who benefits most from your agent: -| Agent Type | Recommended Collections | -|----------------------|------------------------------------------------| -| Task workflow agents | `hve-core-all`, `rpi` | -| Architecture agents | `hve-core-all`, `project-planning` | -| Documentation agents | `hve-core-all`, `prompt-engineering` | -| Data science agents | `hve-core-all`, `data-science` | -| ADO/work item agents | `hve-core-all`, `ado`, `project-planning` | -| Code review agents | `hve-core-all`, `coding-standards` | +| Agent Type | Recommended Collections | +|----------------------|-------------------------------------------| +| Task workflow agents | `hve-core-all`, `rpi` | +| Architecture agents | `hve-core-all`, `project-planning` | +| Documentation agents | `hve-core-all`, `prompt-engineering` | +| Data science agents | `hve-core-all`, `data-science` | +| ADO/work item agents | `hve-core-all`, `ado`, `project-planning` | +| Code review agents | `hve-core-all`, `coding-standards` | ### Declaring Agent Dependencies diff --git a/docs/contributing/prompts.md b/docs/contributing/prompts.md index 712c036b..049986cf 100644 --- a/docs/contributing/prompts.md +++ b/docs/contributing/prompts.md @@ -131,14 +131,14 @@ items: Choose collections based on who invokes or benefits from the workflow: -| Prompt Type | Recommended Collections | -|-------------------------|----------------------------------------------| -| Git/PR workflows | `hve-core-all`, `git` | -| ADO work item workflows | `hve-core-all`, `ado`, `project-planning` | -| GitHub issue workflows | `hve-core-all`, `github` | -| RPI workflow prompts | `hve-core-all`, `rpi` | -| Documentation workflows | `hve-core-all`, `prompt-engineering` | -| Architecture prompts | `hve-core-all`, `project-planning` | +| Prompt Type | Recommended Collections | +|-------------------------|-------------------------------------------| +| Git/PR workflows | `hve-core-all`, `git` | +| ADO work item workflows | `hve-core-all`, `ado`, `project-planning` | +| GitHub issue workflows | `hve-core-all`, `github` | +| RPI workflow prompts | `hve-core-all`, `rpi` | +| Documentation workflows | `hve-core-all`, `prompt-engineering` | +| Architecture prompts | `hve-core-all`, `project-planning` | For complete collection documentation, see [AI Artifacts Common Standards - Collection Manifests](ai-artifacts-common.md#collection-manifests). diff --git a/docs/contributing/skills.md b/docs/contributing/skills.md index a581234b..d47c21f5 100644 --- a/docs/contributing/skills.md +++ b/docs/contributing/skills.md @@ -109,13 +109,13 @@ items: Choose collections based on who uses the skill's utilities: -| Skill Type | Recommended Collections | -|----------------------|--------------------------------------------------| -| Media processing | `hve-core-all` | -| Documentation tools | `hve-core-all`, `prompt-engineering` | -| Data processing | `hve-core-all`, `data-science` | -| Infrastructure tools | `hve-core-all`, `coding-standards` | -| Code generation | `hve-core-all`, `coding-standards` | +| Skill Type | Recommended Collections | +|----------------------|--------------------------------------| +| Media processing | `hve-core-all` | +| Documentation tools | `hve-core-all`, `prompt-engineering` | +| Data processing | `hve-core-all`, `data-science` | +| Infrastructure tools | `hve-core-all`, `coding-standards` | +| Code generation | `hve-core-all`, `coding-standards` | For complete collection documentation, see [AI Artifacts Common Standards - Collection Manifests](ai-artifacts-common.md#collection-manifests). From a82dfb573f05352e95b1e6be3c07353aab937822 Mon Sep 17 00:00:00 2001 From: katriendg Date: Fri, 13 Feb 2026 11:54:57 +0100 Subject: [PATCH 62/62] test(scripts): remove unused variable in dry run test for plugin generation --- scripts/tests/plugins/Generate-Plugins.Tests.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/tests/plugins/Generate-Plugins.Tests.ps1 b/scripts/tests/plugins/Generate-Plugins.Tests.ps1 index 024a77cd..eb2d0c13 100644 --- a/scripts/tests/plugins/Generate-Plugins.Tests.ps1 +++ b/scripts/tests/plugins/Generate-Plugins.Tests.ps1 @@ -228,7 +228,6 @@ display: } It 'Supports DryRun mode' { - $dryRunDir = Join-Path $script:tempDir 'plugins/dryrun-test' $result = Invoke-PluginGeneration -RepoRoot $script:tempDir -CollectionIds @('hve-core-all') -DryRun -Channel 'PreRelease' $result.Success | Should -BeTrue }