diff --git a/.cspell.json b/.cspell.json index 879d1fc0..513206d0 100644 --- a/.cspell.json +++ b/.cspell.json @@ -79,6 +79,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/ado-prd-to-wit.agent.md b/.github/agents/ado-prd-to-wit.agent.md index 69fceffd..7143c0fc 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 e2e1844f..2a274f20 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 242cb531..e7d0e2f3 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 9ea39ff9..b3ccc913 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-backlog-manager.agent.md b/.github/agents/github-backlog-manager.agent.md index 6d7f2b07..3b3a58a8 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/agents/github-issue-manager.agent.md b/.github/agents/github-issue-manager.agent.md index 847c361a..470c1316 100644 --- a/.github/agents/github-issue-manager.agent.md +++ b/.github/agents/github-issue-manager.agent.md @@ -1,6 +1,6 @@ --- description: 'Deprecated: replaced by github-backlog-manager.agent.md for GitHub issue and backlog management' -maturity: deprecated +tools: ['execute/getTerminalOutput', 'execute/runInTerminal', 'read', 'edit/createDirectory', 'edit/createFile', 'edit/editFiles', 'search', 'web', 'agent', 'github/*'] --- # GitHub Issue Manager diff --git a/.github/agents/hve-core-installer.agent.md b/.github/agents/hve-core-installer.agent.md index 9049e64f..eb381e9c 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 @@ -1065,8 +1064,8 @@ Copying agents enables local customization and offline use. • github-backlog-manager (GitHub) Options: - [1] Install all agents (recommended) - [2] Install RPI Core only + [1] Install RPI Core only (recommended) + [2] Install by collection [3] Skip agent installation Your choice? (1/2/3) @@ -1075,23 +1074,89 @@ Your choice? (1/2/3) User input handling: -* "1", "all", "install all" → Copy all agents -* "2", "rpi", "rpi core", "core" → Copy RPI Core bundle only +* "1", "rpi", "rpi core", "core" → Copy RPI Core bundle only +* "2", "collection", "by collection" → Proceed to Collection Selection sub-flow * "3", "skip", "none", "no" → Skip to success report * Unclear response → Ask for clarification +### Collection Selection Sub-Flow + +When the user selects option 2, read collection manifests to present available collections. + +#### Step 1: Read collections and build collection agent counts + +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 collection options + + +```text +🎭 Collection Selection + +Choose one or more collections to install agents tailored to your role, more to come in the future. + +| # | Collection | Agents | Description | +|---|------------|--------|---------------------------------| +| 1 | Developer | [N] | Software engineers writing code | + +Enter collection number(s) separated by commas (e.g., "1"): +``` + + +Agent counts `[N]` include agents matching the collection with `stable` maturity. + +User input handling: + +* 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 collection identifier: + +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 + + +```text +📋 Agents for [Collection Name(s)] + +The following [N] agents will be copied: + + • [agent-name-1] - tags: [tag-1, tag-2] + • [agent-name-2] - tags: [tag-1, tag-2] + ... + +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] +> 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, rpi-agent | -| `all` | All 20 agents (see prompt for full list) | +| 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` = 4 files, `all` = all `.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 @@ -1105,8 +1170,11 @@ $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 { + # Collection-based: $selection contains filtered agent names from collection manifest + $collectionAgents + } } # Check for collisions @@ -1125,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 @@ -1188,6 +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 collection id(s) e.g. "developer" or "developer,devops" files = @{}; skip = @() } @@ -1264,6 +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 ?? 'rpi-core')" } else { Write-Host "UPGRADE_MODE=false" } diff --git a/.github/agents/memory.agent.md b/.github/agents/memory.agent.md index e9fed46c..e5fe4a0f 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 674518d0..a98f90e3 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 a6f46d7d..069f2f10 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 91c84e48..f536ea48 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 9cc6c296..99ccfc02 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 9dff7fc0..e822338e 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 4794ac69..d087a76e 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/copilot-instructions.md b/.github/copilot-instructions.md index e9fd39bf..f4ec8238 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -90,6 +90,9 @@ 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. +* 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/.github/instructions/ado-create-pull-request.instructions.md b/.github/instructions/ado-create-pull-request.instructions.md index 78351e35..ee76b230 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 2596c5fe..d44c0750 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 9c38121c..0043a095 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 0a9445c6..0959cb10 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/community-interaction.instructions.md b/.github/instructions/community-interaction.instructions.md index 5d5561f8..713a4ab5 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/csharp/csharp-tests.instructions.md b/.github/instructions/csharp/csharp-tests.instructions.md index 2c1b08d3..034c78a8 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 4a885448..63ac5d45 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/github-backlog-discovery.instructions.md b/.github/instructions/github-backlog-discovery.instructions.md index 8a98fb96..d7c028e0 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 0964c735..e08fdb4a 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 e87f0f8c..febd048e 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 31157a1b..af4a378d 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/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 bcb2102c..1f41a065 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..78b3f3af 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 `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 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 e7f5f1b3..668e2c42 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 444b6b61..35b8f49d 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/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/.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 88551449..e40121ff 100644 --- a/.github/prompts/github-add-issue.prompt.md +++ b/.github/prompts/github-add-issue.prompt.md @@ -2,7 +2,6 @@ description: 'Create a GitHub issue using discovered repository templates and conversational field collection' agent: 'github-backlog-manager' argument-hint: "[templateName=...] [title=...] [labels=...]" -maturity: experimental --- # Add GitHub Issue 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/.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 1e30a374..8367ec18 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 @@ -135,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. @@ -160,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, analyze frontmatter for maturity levels: +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: - * 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) +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 | |----------------|-------------|---------------------------|---------------------------------| @@ -176,7 +173,7 @@ After detecting GHCP files from Change Type Detection, analyze frontmatter for m | 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 @@ -211,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/.github/prompts/risk-register.prompt.md b/.github/prompts/risk-register.prompt.md index 9f2f167e..e55f5e27 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/.github/workflows/extension-package.yml b/.github/workflows/extension-package.yml index c9b1997d..3bc4e7f1 100644 --- a/.github/workflows/extension-package.yml +++ b/.github/workflows/extension-package.yml @@ -4,44 +4,126 @@ 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' - value: ${{ jobs.package.outputs.version }} - vsix-file: - description: 'Path to the packaged VSIX file' - value: ${{ jobs.package.outputs.vsix-file }} + 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)" + value: ${{ jobs.discover-collections.outputs.matrix }} permissions: contents: read jobs: - package: + discover-collections: + name: Discover Collection Manifests runs-on: ubuntu-latest permissions: contents: read outputs: - version: ${{ steps.package.outputs.version }} - vsix-file: ${{ steps.package.outputs.vsix-file }} + 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: Setup PowerShell modules + shell: pwsh + run: | + Install-Module -Name PowerShell-Yaml -Force -Scope CurrentUser + + - name: Resolve effective version + id: read-version + shell: bash + run: | + version="${{ inputs.version }}" + dev_patch="${{ inputs.dev-patch-number }}" + if [ -z "$version" ]; then + version=$(jq -r .version extension/templates/package.template.json) + fi + if [ -n "$dev_patch" ]; then + version="${version}-dev.${dev_patch}" + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Discover collection manifests + id: discover + shell: pwsh + run: | + Import-Module PowerShell-Yaml -ErrorAction Stop + + $collectionsDir = 'collections' + $channel = '${{ inputs.channel }}'.Trim() + if (-not $channel) { $channel = 'Stable' } + + if (-not (Test-Path $collectionsDir)) { + Write-Error "Collections directory not found: $collectionsDir" + exit 1 + } + + $collectionFiles = Get-ChildItem -Path $collectionsDir -Filter '*.collection.yml' -File | Sort-Object Name + $matrixItems = @() + + 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' } + + if ($maturity -eq 'deprecated') { + Write-Host "::notice::Skipping deprecated collection: $id" + continue + } + + 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 + } + } + + $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 }} + 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 @@ -51,7 +133,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 @@ -76,9 +158,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 +169,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-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/.github/workflows/extension-publish.yml b/.github/workflows/extension-publish.yml index fb4b429c..7840a2df 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: @@ -84,11 +84,14 @@ jobs: contents: read publish: - name: Publish to Marketplace - needs: [prepare-changelog, 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 @@ -116,22 +119,30 @@ 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 + id: publish 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 + # 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 run: | { - echo "## 🎉 Extension Published Successfully" + echo "## 🎉 Extension Published: ${{ matrix.name }}" echo "" - echo "**Version:** ${{ needs.package.outputs.version }}" - echo "**VSIX File:** ${{ needs.package.outputs.vsix-file }}" + echo "**Collection:** ${{ matrix.id }}" + echo "**Version:** ${{ steps.publish.outputs.version }}" 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 8d2ca1bb..0d2ef58a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -98,7 +98,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 @@ -107,11 +107,24 @@ 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 Release Assets + name: Attest and Upload (${{ matrix.id }}) 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.extension-package-release.outputs.collections-matrix) }} permissions: contents: write id-token: write @@ -120,13 +133,13 @@ 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 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: @@ -134,11 +147,47 @@ 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 }}" + 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, upload-plugin-packages] + 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/.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/.gitignore b/.gitignore index 9b539995..2586e4a1 100644 --- a/.gitignore +++ b/.gitignore @@ -418,6 +418,12 @@ FodyWeavers.xsd # Extension build artifacts extension/LICENSE 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 *.cab @@ -442,6 +448,9 @@ checkov-junit.xml pr.md pr-reference.xml +# Dependency pinning scan artifacts +dependency-pinning-artifacts/ + # Copilot tracking .copilot-tracking/ 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/**" ] } 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, 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/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.md b/collections/hve-core-all.collection.md new file mode 100644 index 00000000..17f69cfd --- /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 collection. diff --git a/collections/hve-core-all.collection.yml b/collections/hve-core-all.collection.yml index 97f33db9..136ecd19 100644 --- a/collections/hve-core-all.collection.yml +++ b/collections/hve-core-all.collection.yml @@ -24,6 +24,9 @@ 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 - path: .github/agents/hve-core-installer.agent.md kind: agent - path: .github/agents/memory.agent.md @@ -72,14 +75,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 @@ -120,6 +128,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 @@ -128,16 +137,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/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/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/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/docs/architecture/ai-artifacts.md b/docs/architecture/ai-artifacts.md index 97b53f27..cb083f0b 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 --- @@ -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,12 +73,18 @@ Instructions (`.instructions.md`) encode technology-specific standards and conve --- description: 'Python scripting standards with type hints' applyTo: '**/*.py, **/*.ipynb' -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 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 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. @@ -92,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:** @@ -112,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 --- ``` @@ -122,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 `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. @@ -200,6 +201,119 @@ 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. +## Collection Manifests + +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. + +### Collection Architecture + +```text +┌─────────────────────────────────────────────────────────────────────┐ +│ Collection Manifests │ +│ collections/*.collection.yml │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ items[] │ │ +│ │ - path │ │ +│ │ - kind │ │ +│ │ - maturity (optional, defaults to stable) │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Build System │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Collection │ │ Prepare- │ │ +│ │ Manifests │───▶│ Extension.ps1 │ │ +│ │ *.collection.yml │ -Collection │ │ +│ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Collection Item Structure + +Each collection item defines inclusion metadata for artifact selection and release channel filtering: + +```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 | +|------------|-------------------------------------------------------------------| +| `path` | Repository-relative path to the artifact source | +| `kind` | Artifact type (`agent`, `prompt`, `instruction`, `skill`, `hook`) | +| `maturity` | Optional release channel gating value (`stable` default) | + +### Collection Model + +Collections represent role-targeted artifact packages. Collection manifests select artifacts for those roles. + +| 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 collections for targeted distribution. + +### Collection Build System + +Collections define role-filtered artifact packages. Each collection manifest specifies which artifacts to include and controls release channel eligibility through a `maturity` field: + +```json +{ + "id": "developer", + "name": "hve-developer", + "displayName": "HVE Core - Developer Edition", + "description": "AI-powered coding agents curated for software engineers", + "maturity": "stable", + "items": ["developer"] +} +``` + +The build system resolves collections by: + +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` collection artifacts as the base +5. Adding collection-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 + +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 collection filter. + ## Extension Integration The VS Code extension discovers and activates AI artifacts through contribution points. @@ -210,10 +324,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 -Each artifact's `maturity:` field controls channel inclusion: +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 | |----------------|----------------|---------------------| @@ -222,6 +336,19 @@ Each artifact's `maturity:` field controls channel inclusion: | `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: + +| 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/architecture/workflows.md b/docs/architecture/workflows.md index 81a7555b..119211b4 100644 --- a/docs/architecture/workflows.md +++ b/docs/architecture/workflows.md @@ -88,12 +88,12 @@ flowchart LR 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] @@ -177,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 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 `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 | +|----------------|--------------|--------------------| +| `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 diff --git a/docs/contributing/ai-artifacts-common.md b/docs/contributing/ai-artifacts-common.md index 6e06ca33..da5ef48b 100644 --- a/docs/contributing/ai-artifacts-common.md +++ b/docs/contributing/ai-artifacts-common.md @@ -85,9 +85,216 @@ 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 +## Collection Manifests + +Collection manifests in `collections/*.collection.yml` are the source of truth for artifact selection and maturity. + +### Collection Purpose + +Collection manifests serve three primary functions: + +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 + +### Collection Structure + +Each manifest contains top-level collection metadata and an `items` array: + +```yaml +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 + maturity: stable + - path: .github/prompts/task-plan.prompt.md + kind: prompt + 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: + +```yaml +- path: .github/agents/rpi-agent.agent.md + kind: agent + maturity: stable +``` + +| 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 a Collection + +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. 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 + +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: + +* Collection manifests +* Extension packaging and distribution +* 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/`). + +## Collection Taxonomy + +Collections represent role-targeted artifact packages for HVE-Core artifacts. The collection system enables role-specific artifact distribution without fragmenting the codebase. + +### 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 Assignment Guidelines + +When assigning collections to artifacts: + +* **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 collection assignments:** + +Adding an artifact to multiple collections means adding its `items[]` entry in each relevant `collections/*.collection.yml`: + +```yaml +# In collections/hve-core-all.collection.yml - Universal +- path: .github/instructions/markdown.instructions.md + kind: instruction + +# In collections/coding-standards.collection.yml - Coding standards +- 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 + +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 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. + +## Dependency Declarations + +Some artifacts require other artifacts to function correctly. Dependency behavior is resolved during packaging. + +### Dependency Types + +| 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. + +### 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 collection items in `collections/*.collection.yml`: + +```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 + +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 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 + +* **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 -All AI artifacts (agents, instructions, prompts) **MUST** include a `maturity` field in frontmatter. +Maturity is defined in `collections/*.collection.yml` under `items[].maturity` and MUST NOT appear in artifact frontmatter. ### Purpose @@ -105,22 +312,25 @@ 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 artifacts **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 * The contributor explicitly intends to target early adopters -### Example +### Setting Maturity + +Add or update the maturity value on each collection item in `collections/*.collection.yml`: ```yaml ---- -description: 'Specialized agent for security analysis' -maturity: 'stable' -tools: ['codebase', 'search'] ---- +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/docs/contributing/custom-agents.md b/docs/contributing/custom-agents.md index 7635de2a..da69fbb7 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) @@ -211,6 +200,40 @@ author: 'microsoft/hve-core' --- ``` +## 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 Collections for Agents + +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` | + +### Declaring Agent Dependencies + +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 collection documentation, see [AI Artifacts Common Standards - Collection Manifests](ai-artifacts-common.md#collection-manifests). + ### 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 55ec8341..dc1815da 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 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` @@ -84,17 +89,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,13 +113,54 @@ 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' --- ``` +## Collection Entry Requirements + +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 collection manifests. See [Repo-Specific Instructions Exclusion](ai-artifacts-common.md#repo-specific-instructions-exclusion) for details. + +### Adding Your Instructions to a Collection + +After creating your instructions file, add an `items[]` entry in each target collection manifest: + +```yaml +items: + - path: .github/instructions/my-language.instructions.md + kind: instruction + maturity: stable +``` + +For instructions in subdirectories, use the path format: + +```yaml +items: + - path: .github/instructions/subdirectory/my-instructions.instructions.md + kind: instruction + maturity: stable +``` + +### Selecting Collections for Instructions + +Choose collections based on who uses the technology or pattern: + +| Instruction Type | Recommended Collections | +|-------------------------|---------------------------------------------------| +| 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`, `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). + ## Content Structure Standards ### Required Sections diff --git a/docs/contributing/prompts.md b/docs/contributing/prompts.md index 7f54be83..049986cf 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' @@ -124,6 +112,36 @@ lastUpdated: '2025-11-19' --- ``` +## Collection Entry Requirements + +All prompts must have matching entries in one or more `collections/*.collection.yml` manifests. Collection entries control distribution and maturity. + +### Adding Your Prompt to a Collection + +After creating your prompt file, add an `items[]` entry in each target collection manifest: + +```yaml +items: + - path: .github/prompts/my-prompt.prompt.md + kind: prompt + maturity: stable +``` + +### 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`, `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). + ## Prompt Content Structure Standards ### Required Sections @@ -429,7 +447,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 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 0b6c2d26..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. @@ -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, 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**: 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` 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 83b5311f..d47c21f5 100644 --- a/docs/contributing/skills.md +++ b/docs/contributing/skills.md @@ -81,21 +81,44 @@ 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 --- ``` +## Collection Entry Requirements + +All skills must have matching entries in one or more `collections/*.collection.yml` manifests. Collection entries control distribution and maturity. + +### Adding Your Skill to a Collection + +After creating your skill package, add an `items[]` entry in each target collection manifest: + +```yaml +items: + - path: .github/skills/my-skill + kind: skill + maturity: stable +``` + +### Selecting Collections for Skills + +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` | + +For complete collection documentation, see [AI Artifacts Common Standards - Collection Manifests](ai-artifacts-common.md#collection-manifests). + ## SKILL.md Content Structure ### Required Sections @@ -260,7 +283,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/docs/getting-started/install.md b/docs/getting-started/install.md index ec6f1eaa..92ef7b24 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -85,6 +85,35 @@ 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 role-based artifact collections tailored to specific roles: + +| 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 | + +> [!NOTE] +> Experimental collections are only available via PreRelease extension builds. The Stable channel includes the Full collection only. + +### 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 (Collection Filtering) + +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 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. + ### Quick Decision Tree ```text diff --git a/extension/.vscodeignore b/extension/.vscodeignore index bf53d93b..7d854cba 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 @@ -12,3 +13,9 @@ !README.md !LICENSE !CHANGELOG.md + +# Exclude collection-specific READMEs (only the canonical README.md ships) +README.*.md + +# Exclude collection-specific package templates (only the canonical package.json ships) +package.*.json diff --git a/extension/PACKAGING.md b/extension/PACKAGING.md index 8822945c..935c0440 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-10 ms.topic: reference --- @@ -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 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 @@ -60,6 +60,60 @@ 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 Collection Manifests] --> 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 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 + CM["Collection Manifests
collections/*.collection.yml"] -->|Get-CollectionManifest| INPUTS + CH[Channel: Stable / PreRelease] -->|Get-AllowedMaturities| 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 collection + globs
Get-CollectionArtifacts"] + PA --> HD["Resolve Handoff Closure
BFS through agent frontmatter"] + HD --> RD["Resolve Requires Dependencies
BFS through collection item 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 +175,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{"Collection
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) @@ -138,7 +217,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`. The `Prepare-Extension.ps1` script generates all collection package files with the correct version before preparing the extension. **Setup Personal Access Token (one-time):** @@ -190,11 +269,11 @@ code --install-extension hve-core-*.vsix ## Version Management -### Update Version in `package.json` +### 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. `Prepare-Extension.ps1` generates all `extension/package.json` and `extension/package.*.json` files from the template before performing artifact discovery. -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 +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 @@ -260,7 +339,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 `collections/*.collection.yml` item entries: | Channel | Included Maturity Levels | |------------|-------------------------------------| @@ -269,11 +348,219 @@ 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 collection-specific packages from a single codebase. + +### Available Collections + +Collection manifests are defined in root `collections/` as YAML files: + +| Collection | Manifest | Description | +|------------|-------------------------------|----------------------------------------| +| Full | `hve-core-all.collection.yml` | All artifacts regardless of collection | +| Developer | `developer.collection.yml` | Software engineering focused artifacts | + +### Collection Package Files + +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` | Collection edition metadata | + +When building a non-default collection, `Prepare-Extension.ps1`: + +1. Backs up `package.json` to `package.json.bak` +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` + +After packaging, `Package-Extension.ps1` restores the canonical `package.json` from backup in its `finally` block. + +#### Version Synchronization + +`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 + +To build a specific collection package: + +```bash +# Build the full collection (default, no template copy) +pwsh ./scripts/extension/Prepare-Extension.ps1 +pwsh ./scripts/extension/Package-Extension.ps1 + +# 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 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 + +For rapid iteration without running the full build pipeline: + +```bash +# 1. Prepare the extension (generates package files and discovers artifacts) +pwsh ./scripts/extension/Prepare-Extension.ps1 -Collection collections/developer.collection.yml + +# 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')" + +# 3. Regenerate clean package files with a fresh prepare +pwsh ./scripts/extension/Prepare-Extension.ps1 +``` + +Generated package files are gitignored. Each `Prepare-Extension.ps1` invocation regenerates them from the template. + +### Collection 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 + CI["Collection Item
path · kind · maturity · requires"] --> PF{"Collection match?
empty items = universal"} + CM["Collection Manifest
items 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 collection item requires blocks
across agents · prompts · instructions · skills"] + REQUIRES --> FINAL[Final Collection Artifact Set] +``` + +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 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 + +To verify artifact inclusion before publishing: + +```bash +# 1. Prepare with collection filtering +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' + +# 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 +``` + +### Troubleshooting Collection Builds + +**Missing artifacts in collection:** + +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 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 collection metadata validation +npm run lint:collections-metadata + +# Validate YAML syntax of collection manifests +npm run lint:yaml +``` + +### Collection Manifest Schema + +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 | +|---------------|----------|-------------------------------------------------------------------------------------------------------| +| `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` | +| `items` | Yes | Array of collection 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 + +To create a new collection: + +1. Create a new collection manifest in `collections/`: + + ```yaml + 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 + path: .github/agents/my-agent.agent.md + maturity: experimental + ``` + +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 + +> [!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) * `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/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/package.json b/extension/package.json deleted file mode 100644 index abba9a44..00000000 --- a/extension/package.json +++ /dev/null @@ -1,381 +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": { - "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" - } - ] - }, - "author": "Microsoft", - "license": "MIT" -} 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/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 cbdd6b29..d122690f 100644 --- a/package.json +++ b/package.json @@ -14,16 +14,18 @@ "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: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: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", 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/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/.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/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/.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/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/.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/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/.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/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/.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/hve-core-all/README.md b/plugins/hve-core-all/README.md index 6b6c6b53..5e7f8121 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 | @@ -88,7 +89,6 @@ copilot plugin install hve-core-all@hve-core | 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 | 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/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/.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/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/.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/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/.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/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/.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/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/release-please-config.json b/release-please-config.json index 191f2c80..e69a4927 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -21,8 +21,24 @@ "extra-files": [ { "type": "json", - "path": "extension/package.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/extension/Package-Extension.ps1 b/scripts/extension/Package-Extension.ps1 index 389fd150..bc168a36 100644 --- a/scripts/extension/Package-Extension.ps1 +++ b/scripts/extension/Package-Extension.ps1 @@ -27,6 +27,14 @@ 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 file (YAML or JSON). When specified, only + 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 @@ -68,7 +76,14 @@ param( [string]$ChangelogPath = "", [Parameter(Mandatory = $false)] - [switch]$PreRelease + [switch]$PreRelease, + + [Parameter(Mandatory = $false)] + [string]$Collection = "", + + [Parameter(Mandatory = $false)] + [Alias('dry-run')] + [switch]$DryRun ) $ErrorActionPreference = 'Stop' @@ -113,36 +128,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. - .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 - ) - - $vsixFileName = "$ExtensionName-$PackageVersion.vsix" - return Join-Path $ExtensionDirectory $vsixFileName -} - function Test-ExtensionManifestValid { <# .SYNOPSIS @@ -268,6 +253,54 @@ function New-PackagingResult { } } +function Get-CollectionReadmePath { + <# + .SYNOPSIS + Resolves the collection-specific README path from a collection manifest. + .DESCRIPTION + 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 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 collection README, or $null if not applicable. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [string]$CollectionPath, + + [Parameter(Mandatory = $true)] + [string]$ExtensionDirectory + ) + + $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 + if ($collectionId -eq 'hve-core-all') { + return $null + } + + $collectionReadmePath = Join-Path $ExtensionDirectory "README.$collectionId.md" + if (Test-Path $collectionReadmePath) { + return $collectionReadmePath + } + + return $null +} + function Get-ResolvedPackageVersion { <# .SYNOPSIS @@ -444,6 +477,145 @@ 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. + .PARAMETER RepoRoot + Absolute path to the repository root. + .PARAMETER ExtensionDirectory + Absolute path to the extension directory. + .PARAMETER PrepareResult + 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, + + [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 -replace '^\.[\\/]', '') + if (-not (Test-Path $srcPath)) { + Write-Warning "Skipping missing collection artifact: $srcPath (referenced by contributes.chatAgents in package.json)" + continue + } + 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 -replace '^\.[\\/]', '') + if (-not (Test-Path $srcPath)) { + 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 + New-Item -Path $destDir -ItemType Directory -Force | Out-Null + 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 -replace '^\.[\\/]', '') + if (-not (Test-Path $srcPath)) { + 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 + New-Item -Path $destDir -ItemType Directory -Force | Out-Null + 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 -replace '^\.[\\/]', '') + if (-not (Test-Path $srcPath)) { + 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 + New-Item -Path $destDir -ItemType Directory -Force | Out-Null + Copy-Item -Path $srcPath -Destination $destPath -Recurse -Force + } + } +} + +function Set-CollectionReadme { + <# + .SYNOPSIS + Swaps or restores the collection-specific README for extension packaging. + .DESCRIPTION + 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 CollectionReadmePath + Path to the collection-specific README file. Required for Swap operation. + .PARAMETER Operation + Either 'Swap' to replace README.md with collection content, or 'Restore' + to revert README.md from backup. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ExtensionDirectory, + + [Parameter(Mandatory = $false)] + [string]$CollectionReadmePath = "", + + [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 $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 $CollectionReadmePath -Destination $readmePath -Force + Write-Host " Swapped README.md with $(Split-Path $CollectionReadmePath -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 @@ -597,6 +769,12 @@ 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 file (YAML or JSON). 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. #> @@ -621,7 +799,13 @@ function Invoke-PackageExtension { [string]$ChangelogPath = "", [Parameter(Mandatory = $false)] - [switch]$PreRelease + [switch]$PreRelease, + + [Parameter(Mandatory = $false)] + [string]$Collection = "", + + [Parameter(Mandatory = $false)] + [switch]$DryRun ) $dirsToClean = @(".github", "docs", "scripts") @@ -713,25 +897,72 @@ 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 } } Write-Host " ✅ Extension directory prepared" -ForegroundColor Green + # Swap collection README if collection specifies one + if ($Collection -and $Collection -ne "") { + $collectionReadmePath = Get-CollectionReadmePath -CollectionPath $Collection -ExtensionDirectory $ExtensionDirectory + if ($collectionReadmePath) { + Write-Host "" + Write-Host "📄 Applying collection README..." -ForegroundColor Yellow + Set-CollectionReadme -ExtensionDirectory $ExtensionDirectory -CollectionReadmePath $collectionReadmePath -Operation Swap + } + } + + 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) { @@ -791,6 +1022,20 @@ function Invoke-PackageExtension { return New-PackagingResult -Success $false -ErrorMessage $_.Exception.Message } finally { + # 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 + 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 collection README if it was swapped + Set-CollectionReadme -ExtensionDirectory $ExtensionDirectory -Operation Restore + # Cleanup copied directories using I/O function Write-Host "" Write-Host "🧹 Cleaning up..." -ForegroundColor Yellow @@ -820,7 +1065,9 @@ if ($MyInvocation.InvocationName -ne '.') { -Version $Version ` -DevPatchNumber $DevPatchNumber ` -ChangelogPath $ChangelogPath ` - -PreRelease:$PreRelease + -PreRelease:$PreRelease ` + -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 7d53d5ca..c2c84bd1 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' @@ -62,6 +65,435 @@ 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 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 collection 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 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 collection + 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 + + # 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) + $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 { <# .SYNOPSIS @@ -88,53 +520,434 @@ function Get-AllowedMaturities { return @('stable') } -function Get-FrontmatterData { +function Test-CollectionMaturityEligible { <# .SYNOPSIS - Extracts description and maturity from YAML frontmatter. + Checks whether a collection is eligible for the specified release channel. .DESCRIPTION - Function that parses YAML frontmatter from a markdown file - and returns a hashtable with description and maturity values. - .PARAMETER FilePath - Path to the markdown file to parse. - .PARAMETER FallbackDescription - Default description if none found in frontmatter. + 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 description and maturity keys. + [hashtable] With IsEligible bool and Reason string. #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] - [string]$FilePath, + [hashtable]$CollectionManifest, - [Parameter(Mandatory = $false)] - [string]$FallbackDescription = "" + [Parameter(Mandatory = $true)] + [ValidateSet('Stable', 'PreRelease')] + [string]$Channel ) - $content = Get-Content -Path $FilePath -Raw - $description = "" - $maturity = "stable" + $maturity = 'stable' + if ($CollectionManifest.ContainsKey('maturity') -and $CollectionManifest['maturity']) { + $maturity = $CollectionManifest['maturity'] + } - 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 + switch ($maturity) { + 'deprecated' { + return @{ + IsEligible = $false + Reason = "Collection '$($CollectionManifest.id)' is deprecated and excluded from all channels" } - if ($data.ContainsKey('maturity')) { - $maturity = $data.maturity + } + 'experimental' { + if ($Channel -eq 'Stable') { + return @{ + IsEligible = $false + Reason = "Collection '$($CollectionManifest.id)' is experimental and excluded from Stable channel" + } } + return @{ IsEligible = $true; Reason = '' } } - catch { - Write-Warning "Failed to parse YAML frontmatter in $(Split-Path -Leaf $FilePath): $_" + '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-CollectionManifest { + <# + .SYNOPSIS + Loads a collection manifest from a YAML or JSON file. + .DESCRIPTION + 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, items, 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" + } + + $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 +} + +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-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 + ) + + if ($CollectionItem.ContainsKey('maturity') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionItem.maturity)) { + return [string]$CollectionItem.maturity + } + + return 'stable' +} + +function Get-CollectionArtifacts { + <# + .SYNOPSIS + 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 used when + present; otherwise artifacts default to stable. + .PARAMETER Collection + Collection manifest hashtable with items. + .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]$Collection, + + [Parameter(Mandatory = $true)] + [string[]]$AllowedMaturities + ) + + $result = @{ + Agents = @() + Prompts = @() + Instructions = @() + Skills = @() + } + + if (-not $Collection.ContainsKey('items') -or @($Collection.items).Count -eq 0) { + return $result + } + + foreach ($item in $Collection.items) { + if (-not $item.ContainsKey('kind') -or -not $item.ContainsKey('path')) { + continue + } + + $kind = [string]$item.kind + $path = [string]$item.path + + $maturity = Get-CollectionArtifactMaturity -CollectionItem $item + if ($AllowedMaturities -notcontains $maturity) { + continue + } + + $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 } + } + } + + 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. + .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 + ) + + $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 + } + + # 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) { + # 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 + } elseif ($handoff -is [hashtable] -and $handoff.ContainsKey('agent')) { + $targetAgent = $handoff.agent + } + if ($targetAgent -and $visited.Add($targetAgent)) { + $queue.Enqueue($targetAgent) + } + } + } + } + catch { + Write-Warning "Failed to parse handoffs from $current.agent.md: $_" + } + } + } + + return @($visited) +} + +function Resolve-RequiresDependencies { + <# + .SYNOPSIS + Resolves transitive artifact dependencies from collection item requires blocks. + .DESCRIPTION + 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 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. + .OUTPUTS + [hashtable] With Agents, Prompts, Instructions, Skills arrays containing resolved names. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [hashtable]$ArtifactNames, + + [Parameter(Mandatory = $true)] + [string[]]$AllowedMaturities, + + [Parameter(Mandatory = $false)] + [hashtable]$CollectionRequires = @{}, + + [Parameter(Mandatory = $false)] + [hashtable]$CollectionMaturities = @{} + ) + + $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) + } + } + } + + $changed = $true + while ($changed) { + $changed = $false + + foreach ($sourceType in @('agents', 'prompts', 'instructions', 'skills')) { + if (-not $CollectionRequires.ContainsKey($sourceType)) { + continue + } + + $sourceCapitalType = $typeMap[$sourceType] + foreach ($sourceName in @($resolved[$sourceCapitalType])) { + if (-not $CollectionRequires[$sourceType].ContainsKey($sourceName)) { + continue + } + + $requires = $CollectionRequires[$sourceType][$sourceName] + if (-not $requires) { + continue + } + + foreach ($targetType in @('agents', 'prompts', 'instructions', 'skills')) { + if (-not $requires.ContainsKey($targetType)) { + continue + } + + $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 ($resolved[$targetCapitalType].Add($dep)) { + $changed = $true + } + } + } + } } } + # Convert HashSets to arrays return @{ - description = if ($description) { $description } else { $FallbackDescription } - maturity = $maturity + Agents = @($resolved.Agents) + Prompts = @($resolved.Prompts) + Instructions = @($resolved.Instructions) + Skills = @($resolved.Skills) } } @@ -196,8 +1009,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, - and returns structured agent objects. + filters by exclusion list, and returns structured agent objects. .PARAMETER AgentsDir Path to the agents directory. .PARAMETER AllowedMaturities @@ -240,8 +1052,7 @@ function Get-DiscoveredAgents { continue } - $frontmatter = Get-FrontmatterData -FilePath $agentFile.FullName -FallbackDescription "AI agent for $agentName" - $maturity = $frontmatter.maturity + $maturity = "stable" if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $agentName; Reason = "maturity: $maturity" } @@ -249,9 +1060,8 @@ function Get-DiscoveredAgents { } $result.Agents += [PSCustomObject]@{ - name = $agentName - path = "./.github/agents/$($agentFile.Name)" - description = $frontmatter.description + name = $agentName + path = "./.github/agents/$($agentFile.Name)" } } @@ -264,8 +1074,7 @@ 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. + and returns structured prompt objects with relative paths. .PARAMETER PromptsDir Path to the prompts directory. .PARAMETER GitHubDir @@ -302,9 +1111,7 @@ function Get-DiscoveredPrompts { foreach ($promptFile in $promptFiles) { $promptName = $promptFile.BaseName -replace '\.prompt$', '' - $displayName = ($promptName -replace '-', ' ') -replace '(\b\w)', { $_.Groups[1].Value.ToUpper() } - $frontmatter = Get-FrontmatterData -FilePath $promptFile.FullName -FallbackDescription "Prompt for $displayName" - $maturity = $frontmatter.maturity + $maturity = "stable" if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $promptName; Reason = "maturity: $maturity" } @@ -314,9 +1121,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" } } @@ -329,8 +1135,7 @@ 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. + and returns structured instruction objects with normalized paths. .PARAMETER InstructionsDir Path to the instructions directory. .PARAMETER GitHubDir @@ -366,11 +1171,16 @@ 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() } - $frontmatter = Get-FrontmatterData -FilePath $instrFile.FullName -FallbackDescription "Instructions for $displayName" - $maturity = $frontmatter.maturity + + $maturity = "stable" if ($AllowedMaturities -notcontains $maturity) { $result.Skipped += @{ Name = $instrName; Reason = "maturity: $maturity" } @@ -381,9 +1191,69 @@ function Get-DiscoveredInstructions { $normalizedRelativePath = (Join-Path ".github" $relativePathFromGitHub) -replace '\\', '/' $result.Instructions += [PSCustomObject]@{ - name = $instrName - path = "./$normalizedRelativePath" - description = $frontmatter.description + name = $instrName + path = "./$normalizedRelativePath" + } + } + + 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 and returns structured skill objects. + .PARAMETER SkillsDir + Path to the skills directory. + .PARAMETER AllowedMaturities + Array of maturity levels to include. + .OUTPUTS + [hashtable] With Skills array, Skipped array, and DirectoryExists bool. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string]$SkillsDir, + + [Parameter(Mandatory = $true)] + [string[]]$AllowedMaturities + ) + + $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 ($AllowedMaturities -notcontains $maturity) { + $result.Skipped += @{ Name = $skillName; Reason = "maturity: $maturity" } + continue + } + + $result.Skills += [PSCustomObject]@{ + name = $skillName + path = "./.github/skills/$skillName" } } @@ -396,7 +1266,8 @@ function Update-PackageJsonContributes { 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 +1276,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,12 +1297,22 @@ 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 $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]@{}) @@ -454,6 +1337,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 +1363,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 +1389,9 @@ function New-PrepareResult { [Parameter(Mandatory = $false)] [int]$InstructionCount = 0, + [Parameter(Mandatory = $false)] + [int]$SkillCount = 0, + [Parameter(Mandatory = $false)] [string]$ErrorMessage = "" ) @@ -508,10 +1402,94 @@ function New-PrepareResult { AgentCount = $AgentCount PromptCount = $PromptCount InstructionCount = $InstructionCount + SkillCount = $SkillCount ErrorMessage = $ErrorMessage } } +function Test-TemplateConsistency { + <# + .SYNOPSIS + Validates collection template metadata against its collection manifest. + .DESCRIPTION + 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 collection 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 @@ -532,7 +1510,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,14 +1531,28 @@ function Invoke-PrepareExtension { [string]$ChangelogPath = "", [Parameter(Mandatory = $false)] - [switch]$DryRun + [switch]$DryRun, + + [Parameter(Mandatory = $false)] + [string]$Collection = "" ) # Derive paths $GitHubDir = Join-Path $RepoRoot ".github" $PackageJsonPath = Join-Path $ExtensionDirectory "package.json" - # Validate required paths exist + # 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) collection 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 @@ -600,9 +1592,103 @@ function Invoke-PrepareExtension { Write-Host "[DRY RUN] No changes will be made" -ForegroundColor Yellow } - # Discover agents + # Load collection manifest if specified + $collectionManifest = $null + $collectionArtifactNames = $null + $collectionMaturities = @{} + $collectionRequires = @{} + + 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) { + # 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 for items: $rootCollectionPath" + } + else { + Write-Warning "No root collection found for '$($collectionManifest.id)' at $rootCollectionPath" + } + } + + # 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" + + # Build collection maturity map and channel-filtered artifact names + $collectionMaturities = @{} + $collectionRequires = @{} + + 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 + 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 -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 + $collectionArtifactNames.Agents = $expandedAgents + } + + # Resolve requires dependencies + $resolvedNames = Resolve-RequiresDependencies -ArtifactNames @{ + agents = $collectionArtifactNames.Agents + prompts = $collectionArtifactNames.Prompts + instructions = $collectionArtifactNames.Instructions + skills = $collectionArtifactNames.Skills + } -AllowedMaturities $allowedMaturities -CollectionRequires $collectionRequires -CollectionMaturities $collectionMaturities + + $collectionArtifactNames = @{ + Agents = $resolvedNames.Agents + Prompts = $resolvedNames.Prompts + Instructions = $resolvedNames.Instructions + Skills = $resolvedNames.Skills + } + } + + # 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 @() + $agentResult = Get-DiscoveredAgents -AgentsDir $agentsDir -AllowedMaturities $discoveryAllowedMaturities -ExcludedAgents @() $chatAgents = $agentResult.Agents $excludedAgents = $agentResult.Skipped @@ -614,7 +1700,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 $discoveryAllowedMaturities $chatPrompts = $promptResult.Prompts $excludedPrompts = $promptResult.Skipped @@ -626,7 +1712,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 $discoveryAllowedMaturities $chatInstructions = $instructionResult.Instructions $excludedInstructions = $instructionResult.Skipped @@ -636,11 +1722,72 @@ function Invoke-PrepareExtension { Write-Host "Excluded $($excludedInstructions.Count) instruction(s) due to maturity filter" -ForegroundColor Yellow } - # Update package.json + # Discover skills + $skillsDir = Join-Path $GitHubDir "skills" + $skillResult = Get-DiscoveredSkills -SkillsDir $skillsDir -AllowedMaturities $discoveryAllowedMaturities + $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)" + } + + # 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 "Collection 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 + + # 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 collection template: package.$collectionId.json" -ForegroundColor Green + } + + # Update package.json with generated contributes $packageJson = Update-PackageJsonContributes -PackageJson $packageJson ` -ChatAgents $chatAgents ` -ChatPromptFiles $chatPrompts ` - -ChatInstructions $chatInstructions + -ChatInstructions $chatInstructions ` + -ChatSkills $chatSkills # Write updated package.json if (-not $DryRun) { @@ -672,7 +1819,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 +1853,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 +1864,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 +1878,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/Validate-MarkdownFrontmatter.ps1 b/scripts/linting/Validate-MarkdownFrontmatter.ps1 index 41b149e6..76e86d6c 100644 --- a/scripts/linting/Validate-MarkdownFrontmatter.ps1 +++ b/scripts/linting/Validate-MarkdownFrontmatter.ps1 @@ -31,6 +31,9 @@ param( [string[]]$ExcludePaths = @( '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/**' 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/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/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 diff --git a/scripts/plugins/Generate-Plugins.ps1 b/scripts/plugins/Generate-Plugins.ps1 index 0dcc0fcc..b1331e49 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,12 +186,20 @@ 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' $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)" @@ -130,6 +224,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 +252,15 @@ 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 ` + -Version $repoVersion ` -DryRun:$DryRun - $itemCount = $collection.items.Count + $itemCount = $filteredCollection.items.Count $totalAgents += $result.AgentCount $totalCommands += $result.CommandCount $totalInstructions += $result.InstructionCount @@ -172,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" @@ -210,7 +314,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..494ffbc7 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,30 @@ 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' } + + # Exclude repo-specific artifacts under .github/**/hve-core/ + if ($relativePath -match '^\.github/.*/hve-core/') { + continue + } + + $items += @{ path = $relativePath; kind = $kind } } } @@ -202,20 +225,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. + Reads maturity from the provided collection item metadata value and + returns $true when the effective value equals deprecated. - .PARAMETER ItemPath - Repo-relative path to the artifact. - - .PARAMETER Kind - The artifact kind: agent, prompt, instruction, or skill. - - .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 +240,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 +288,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 @@ -439,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. @@ -447,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. #> @@ -457,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 } } @@ -544,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 @@ -644,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. @@ -663,6 +838,9 @@ function Write-PluginDirectory { [Parameter(Mandatory = $true)] [string]$RepoRoot, + [Parameter(Mandatory = $true)] + [string]$Version, + [Parameter(Mandatory = $false)] [switch]$DryRun ) @@ -730,10 +908,34 @@ 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' - $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" @@ -766,17 +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' ) diff --git a/scripts/plugins/Validate-Collections.ps1 b/scripts/plugins/Validate-Collections.ps1 index 86aa0370..b4454c69 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,16 @@ 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 + + # 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)) { @@ -190,6 +263,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 +302,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" diff --git a/scripts/tests/extension/Package-Extension.Tests.ps1 b/scripts/tests/extension/Package-Extension.Tests.ps1 index 86198ec2..69a53da2 100644 --- a/scripts/tests/extension/Package-Extension.Tests.ps1 +++ b/scripts/tests/extension/Package-Extension.Tests.ps1 @@ -62,24 +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 - } -} - Describe 'Test-ExtensionManifestValid' { It 'Returns valid result for proper manifest' { $manifest = [PSCustomObject]@{ @@ -293,6 +275,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' @@ -479,6 +462,175 @@ 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-test-collection' + version = '2.5.0' + 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-test-collection-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' + } + } + + 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' { @@ -492,6 +644,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 '{}' @@ -768,6 +921,330 @@ Describe 'Restore-PackageJsonVersion' { } } +Describe 'Get-CollectionReadmePath' { + BeforeAll { + $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' + } + + 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.yml' + @" +id: hve-core-all +name: all +"@ | Set-Content $collectionPath + + $result = Get-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir + $result | Should -BeNullOrEmpty + } + + It 'Returns collection README path when file exists' { + $collectionPath = Join-Path $script:testDir 'collection.yml' + @" +id: developer +name: dev +"@ | Set-Content $collectionPath + + $collectionReadme = Join-Path $script:extDir 'README.developer.md' + Set-Content -Path $collectionReadme -Value '# Developer README' + + $result = Get-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir + $result | Should -Be $collectionReadme + } + + 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-CollectionReadmePath -CollectionPath $collectionPath -ExtensionDirectory $script:extDir + $result | Should -BeNullOrEmpty + } +} + +Describe 'Set-CollectionReadme' { + 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 collection README and creates backup' { + $collectionReadmePath = Join-Path $script:testDir 'README.developer.md' + Set-Content -Path $collectionReadmePath -Value '# Developer README' + + 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' + + 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 collection path for swap' { + Mock Write-Warning {} + Set-CollectionReadme -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 '# Collection README' + + Set-CollectionReadme -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-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' + } +} + +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.yml' + @" +id: developer +name: dev +displayName: Developer +items: + - developer +"@ | 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 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') } } + + $collectionPath = Join-Path $script:testRoot 'collection.yml' + @" +id: developer +name: dev +displayName: Developer +items: + - developer +"@ | Set-Content $collectionPath + + # 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' + + $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))" @@ -787,6 +1264,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 +1356,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..29849675 100644 --- a/scripts/tests/extension/Prepare-Extension.Tests.ps1 +++ b/scripts/tests/extension/Prepare-Extension.Tests.ps1 @@ -6,22 +6,65 @@ BeforeAll { . $PSScriptRoot/../../extension/Prepare-Extension.ps1 } -Describe 'Get-AllowedMaturities' { - It 'Returns only stable for Stable channel' { - $result = Get-AllowedMaturities -Channel 'Stable' - $result | Should -Be @('stable') +#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 'Returns all maturities for PreRelease channel' { - $result = Get-AllowedMaturities -Channel 'PreRelease' - $result | Should -Contain 'stable' - $result | Should -Contain 'preview' - $result | Should -Contain 'experimental' + 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 'Get-FrontmatterData' { +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 @@ -31,45 +74,443 @@ Describe 'Get-FrontmatterData' { Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue } - It 'Extracts description and maturity from frontmatter' { - $testFile = Join-Path $script:tempDir 'test.md' - @' + 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-collection 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 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 + $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 collection 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 + } +} + +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: "Test description" -maturity: preview +description: "Alpha agent description" --- -# Content -'@ | Set-Content -Path $testFile +# Alpha +"@ | Set-Content -Path (Join-Path $agentsDir 'alpha.agent.md') - $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'fallback' - $result.description | Should -Be 'Test description' - $result.maturity | Should -Be 'preview' - } + @" +--- +description: "Zebra agent description" +--- +# Zebra +"@ | Set-Content -Path (Join-Path $agentsDir 'zebra.agent.md') - It 'Uses fallback description when not in frontmatter' { - $testFile = Join-Path $script:tempDir 'no-desc.md' - @' + @" --- -maturity: stable +description: "My prompt description" --- -# Content -'@ | Set-Content -Path $testFile +# Prompt +"@ | Set-Content -Path (Join-Path $promptsDir 'my-prompt.prompt.md') - $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'My Fallback' - $result.description | Should -Be 'My Fallback' - } + @" +--- +description: "My instruction description" +applyTo: "**/*.ps1" +--- +# Instruction +"@ | Set-Content -Path (Join-Path $instrDir 'my-instr.instructions.md') - It 'Defaults maturity to stable when not specified' { - $testFile = Join-Path $script:tempDir 'no-maturity.md' - @' + @" --- -description: "Desc" +name: my-skill +description: "My skill description" --- -# Content -'@ | Set-Content -Path $testFile +# 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 non-default collections' { + $collection = @{ + id = 'test-edition' + name = 'Test Edition' + description = 'Test edition test' + items = @() + } + $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 + + $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' { + It 'Returns only stable for Stable channel' { + $result = Get-AllowedMaturities -Channel 'Stable' + $result | Should -Be @('stable') + } + + It 'Returns all maturities for PreRelease channel' { + $result = Get-AllowedMaturities -Channel 'PreRelease' + $result | Should -Contain 'stable' + $result | Should -Contain 'preview' + $result | Should -Contain 'experimental' + } + +} + +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' + } - $result = Get-FrontmatterData -FilePath $testFile -FallbackDescription 'fallback' - $result.maturity | Should -Be 'stable' + 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' } } @@ -122,16 +563,15 @@ 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') + } AfterAll { @@ -145,9 +585,9 @@ maturity: preview } It 'Filters agents by maturity' { - $result = Get-DiscoveredAgents -AgentsDir $script:agentsDir -AllowedMaturities @('stable') -ExcludedAgents @() - $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' { @@ -174,7 +614,6 @@ Describe 'Get-DiscoveredPrompts' { @' --- description: "Test prompt" -maturity: stable --- '@ | Set-Content -Path (Join-Path $script:promptsDir 'test.prompt.md') } @@ -208,7 +647,6 @@ Describe 'Get-DiscoveredInstructions' { --- description: "Test instruction" applyTo: "**/*.ps1" -maturity: stable --- '@ | Set-Content -Path (Join-Path $script:instrDir 'test.instructions.md') } @@ -228,85 +666,468 @@ maturity: stable $result = Get-DiscoveredInstructions -InstructionsDir $nonexistentPath -GitHubDir $script:ghDir -AllowedMaturities @('stable') $result.DirectoryExists | Should -BeFalse } -} -Describe 'Update-PackageJsonContributes' { - It 'Updates contributes section with chat participants' { - $packageJson = [PSCustomObject]@{ - name = 'test-extension' - contributes = [PSCustomObject]@{} - } - $agents = @( - @{ name = 'agent1'; description = 'Desc 1' } - ) - $prompts = @( - @{ name = 'prompt1'; description = 'Prompt desc' } - ) - $instructions = @( - @{ name = 'instr1'; description = 'Instr desc' } - ) + 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 = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents $agents -ChatPromptFiles $prompts -ChatInstructions $instructions - $result.contributes | Should -Not -BeNullOrEmpty + $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 'Handles empty arrays' { - $packageJson = [PSCustomObject]@{ - name = 'test-extension' - contributes = [PSCustomObject]@{} - } + 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 = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents @() -ChatPromptFiles @() -ChatInstructions @() - $result | Should -Not -BeNullOrEmpty + $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 'New-PrepareResult' { - It 'Creates success result with counts' { - $result = New-PrepareResult -Success $true -AgentCount 5 -PromptCount 10 -InstructionCount 15 -Version '1.0.0' - $result.Success | Should -BeTrue - $result.AgentCount | Should -Be 5 - $result.PromptCount | Should -Be 10 - $result.InstructionCount | Should -Be 15 - $result.Version | Should -Be '1.0.0' - $result.ErrorMessage | Should -BeNullOrEmpty +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 + } - It 'Creates failure result with error message' { - $result = New-PrepareResult -Success $false -ErrorMessage 'Something went wrong' - $result.Success | Should -BeFalse - $result.ErrorMessage | Should -Be 'Something went wrong' - $result.AgentCount | Should -Be 0 - $result.PromptCount | Should -Be 0 - $result.InstructionCount | Should -Be 0 + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue } - It 'Returns hashtable with all expected keys' { - $result = New-PrepareResult -Success $true - $result.Keys | Should -Contain 'Success' - $result.Keys | Should -Contain 'AgentCount' - $result.Keys | Should -Contain 'PromptCount' - $result.Keys | Should -Contain 'InstructionCount' - $result.Keys | Should -Contain 'Version' - $result.Keys | Should -Contain 'ErrorMessage' + It 'Discovers skills in directory' { + $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' + } + + 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 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') + $skippedNames = $result.Skipped | ForEach-Object { $_.Name } + $skippedNames | Should -Contain 'empty-skill' } } -Describe 'Invoke-PrepareExtension' { +Describe 'Get-CollectionManifest' { BeforeAll { - $script:tempDir = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString()) + $script:tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null + } - # Create extension directory with package.json - $script:extDir = Join-Path $script:tempDir 'extension' - New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null - @' -{ - "name": "test-extension", - "version": "1.2.3", - "contributes": {} -} -'@ | Set-Content -Path (Join-Path $script:extDir 'package.json') + AfterAll { + Remove-Item -Path $script:tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + 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 +items: + - 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-manifest.schema.json' + id = 'test' + name = 'test-ext' + displayName = 'Test Extension' + description = 'Test' + items = @('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.yml' + @" +id: keys +name: keys-ext +displayName: Keys +description: Keys test +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 'items' + } +} + +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' { + 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 -Collection $collection -AllowedMaturities @('stable', 'preview') + $result.Agents | Should -Contain 'dev-agent' + $result.Prompts | Should -Contain 'dev-prompt' + $result.Instructions | Should -Contain 'dev/dev' + $result.Skills | Should -Contain 'video-to-gif' + } + + It 'Uses item maturity when provided' { + $collection = @{ + 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 -Collection $collection -AllowedMaturities @('stable') + $result.Agents | Should -Contain 'dev-agent' + $result.Agents | Should -Not -Contain 'preview-dev' + } + + It 'Defaults to stable maturity when item maturity is omitted' { + $collection = @{ + items = @( + @{ kind = 'agent'; path = '.github/agents/dev-agent.agent.md' }, + @{ kind = 'agent'; path = '.github/agents/preview-dev.agent.md' } + ) + } + + $result = Get-CollectionArtifacts -Collection $collection -AllowedMaturities @('stable') + $result.Agents | Should -Contain 'dev-agent' + $result.Agents | Should -Contain 'preview-dev' + } + + It 'Returns empty when collection has no items' { + $collection = @{ id = 'empty' } + $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 + $result.Skills.Count | Should -Be 0 + } +} + +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 (object format matching real agents) + @' +--- +description: "Parent agent" +handoffs: + - label: "Go to child" + agent: child + prompt: Continue +--- +'@ | 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 (object format) + @' +--- +description: "Self agent" +handoffs: + - label: "Self" + agent: self-ref +--- +'@ | Set-Content -Path (Join-Path $script:agentsDir 'self-ref.agent.md') + + # Circular chain (object format) + @' +--- +description: "Chain A" +handoffs: + - label: "To B" + agent: chain-b +--- +'@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-a.agent.md') + + @' +--- +description: "Chain B" +handoffs: + - label: "To A" + agent: chain-a +--- +'@ | Set-Content -Path (Join-Path $script:agentsDir 'chain-b.agent.md') + + } + + 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 + $result | Should -Contain 'solo' + $result.Count | Should -Be 1 + } + + It 'Resolves single-level handoff' { + $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 + $result | Should -Contain 'self-ref' + $result.Count | Should -Be 1 + } + + It 'Handles circular handoff chains' { + $result = Resolve-HandoffDependencies -SeedAgents @('chain-a') -AgentsDir $script:agentsDir + $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' { + $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' { + $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' { + $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' + } +} + +Describe 'Update-PackageJsonContributes' { + It 'Updates contributes section with chat participants' { + $packageJson = [PSCustomObject]@{ + name = 'test-extension' + contributes = [PSCustomObject]@{} + } + $agents = @( + @{ name = 'agent1'; description = 'Desc 1' } + ) + $prompts = @( + @{ name = 'prompt1'; description = 'Prompt desc' } + ) + $instructions = @( + @{ name = 'instr1'; description = 'Instr desc' } + ) + + $result = Update-PackageJsonContributes -PackageJson $packageJson -ChatAgents $agents -ChatPromptFiles $prompts -ChatInstructions $instructions -ChatSkills @() + $result.contributes | Should -Not -BeNullOrEmpty + } + + It 'Handles empty arrays' { + $packageJson = [PSCustomObject]@{ + name = 'test-extension' + contributes = [PSCustomObject]@{} + } + + $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 -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 + } + + It 'Creates failure result with error message' { + $result = New-PrepareResult -Success $false -ErrorMessage 'Something went wrong' + $result.Success | Should -BeFalse + $result.ErrorMessage | Should -Be 'Something went wrong' + $result.AgentCount | Should -Be 0 + $result.PromptCount | Should -Be 0 + $result.InstructionCount | Should -Be 0 + } + + It 'Returns hashtable with all expected keys' { + $result = New-PrepareResult -Success $true + $result.Keys | Should -Contain 'Success' + $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' + } +} + +Describe 'Invoke-PrepareExtension' { + BeforeAll { + $script:tempDir = Join-Path $TestDrive ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path $script:tempDir -Force | Out-Null + + # Create extension directory with package.json + $script:extDir = Join-Path $script:tempDir 'extension' + New-Item -ItemType Directory -Path $script:extDir -Force | Out-Null + @' +{ + "name": "test-extension", + "version": "1.2.3", + "contributes": {} +} +'@ | 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' @@ -321,7 +1142,6 @@ Describe 'Invoke-PrepareExtension' { @' --- description: "Test agent" -maturity: stable --- # Agent '@ | Set-Content -Path (Join-Path $script:agentsDir 'test.agent.md') @@ -330,7 +1150,6 @@ maturity: stable @' --- description: "Test prompt" -maturity: stable --- # Prompt '@ | Set-Content -Path (Join-Path $script:promptsDir 'test.prompt.md') @@ -340,10 +1159,10 @@ maturity: stable --- description: "Test instruction" applyTo: "**/*.ps1" -maturity: stable --- # Instruction '@ | Set-Content -Path (Join-Path $script:instrDir 'test.instructions.md') + } AfterAll { @@ -372,7 +1191,7 @@ maturity: stable -Channel 'Stable' $result.Success | Should -BeFalse - $result.ErrorMessage | Should -Match 'Required paths not found' + $result.ErrorMessage | Should -Not -BeNullOrEmpty } It 'Respects channel filtering' { @@ -380,20 +1199,36 @@ maturity: stable @' --- description: "Preview agent" -maturity: preview --- '@ | Set-Content -Path (Join-Path $script:agentsDir 'preview.agent.md') + $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 +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 ` -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 @@ -404,7 +1239,6 @@ maturity: preview @' --- description: "Experimental prompt" -maturity: experimental --- '@ | Set-Content -Path (Join-Path $script:promptsDir 'experimental.prompt.md') @@ -413,23 +1247,47 @@ maturity: experimental --- description: "Preview instruction" applyTo: "**/*.js" -maturity: preview --- '@ | Set-Content -Path (Join-Path $script:instrDir 'preview.instructions.md') + $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 +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 ` -RepoRoot $script:tempDir ` -Channel 'Stable' ` + -Collection $collectionPath ` -DryRun $preReleaseResult = Invoke-PrepareExtension ` -ExtensionDirectory $script:extDir ` -RepoRoot $script:tempDir ` -Channel 'PreRelease' ` + -Collection $collectionPath ` -DryRun - # Stable should have fewer prompts and instructions than PreRelease $preReleaseResult.PromptCount | Should -BeGreaterThan $stableResult.PromptCount $preReleaseResult.InstructionCount | Should -BeGreaterThan $stableResult.InstructionCount } @@ -462,36 +1320,571 @@ maturity: preview Test-Path (Join-Path $script:extDir 'CHANGELOG.md') | Should -BeTrue } + 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 $badExtDir ` + -RepoRoot $badRoot ` + -Channel 'Stable' + + $result.Success | Should -BeFalse + $result.ErrorMessage | Should -Match 'Package generation failed' + } + + 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 $emptyExtDir ` + -RepoRoot $emptyRoot ` + -Channel 'Stable' + + $result.Success | Should -BeFalse + $result.ErrorMessage | Should -Match 'Package generation failed' + } + + Context 'Collection template copy' { + BeforeAll { + # Developer collection manifest (in collections/ for generation) + $script:devCollectionYaml = Join-Path $script:collectionsDir 'developer.collection.yml' + @" +id: developer +name: hve-developer +displayName: HVE Core - Developer Edition +description: Developer edition +"@ | 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' + @" +id: hve-core-all +name: hve-core-all +displayName: HVE Core - All +description: All artifacts +"@ | Set-Content -Path $script:allCollectionPath + + # Collection manifest referencing a missing template + $script:missingCollectionPath = Join-Path $script:tempDir 'nonexistent.collection.yml' + @" +id: nonexistent +name: nonexistent +displayName: Nonexistent +description: Missing template +"@ | Set-Content -Path $script:missingCollectionPath + + } + + AfterEach { + # 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 + } + } + + It 'Skips template copy when no collection specified' { + $result = Invoke-PrepareExtension ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` + -Channel 'Stable' ` + -DryRun + + $result.Success | Should -BeTrue + # 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 + } + + 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 collection 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 'Collection 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 + # 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 in collections/ directory for generation + $script:deprecatedCollectionPath = Join-Path $script:collectionsDir 'deprecated-coll.collection.yml' + @" +id: deprecated-coll +name: deprecated-ext +displayName: Deprecated Collection +description: Deprecated collection for testing +maturity: deprecated +"@ | Set-Content -Path $script:deprecatedCollectionPath + + # Experimental collection in collections/ directory for generation + $script:experimentalCollectionPath = Join-Path $script:collectionsDir 'experimental-coll.collection.yml' + @" +id: experimental-coll +name: experimental-ext +displayName: Experimental Collection +description: Experimental collection for testing +maturity: experimental +"@ | Set-Content -Path $script:experimentalCollectionPath + } + + 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.ErrorMessage | Should -Be '' + } + } +} + +#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' { - $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') + # Write invalid JSON and mock generation to preserve it + $badPkgPath = Join-Path $script:extDir 'package.json' + 'NOT VALID JSON' | Set-Content -Path $badPkgPath - # 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 + Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) } $result = Invoke-PrepareExtension ` - -ExtensionDirectory $badJsonDir ` - -RepoRoot (Split-Path $badJsonDir -Parent) ` + -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 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') + 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 - # 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 + Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) } $result = Invoke-PrepareExtension ` - -ExtensionDirectory $noVersionDir ` - -RepoRoot (Split-Path $noVersionDir -Parent) ` + -ExtensionDirectory $script:extDir ` + -RepoRoot $script:tempDir ` -Channel 'Stable' $result.Success | Should -BeFalse @@ -499,20 +1892,212 @@ maturity: preview } 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') + $badPkgPath = Join-Path $script:extDir 'package.json' + @{ name = 'test'; version = 'not-semver' } | ConvertTo-Json | Set-Content -Path $badPkgPath - # 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 + Mock Invoke-ExtensionCollectionsGeneration { return @($badPkgPath) } $result = Invoke-PrepareExtension ` - -ExtensionDirectory $badVersionDir ` - -RepoRoot (Split-Path $badVersionDir -Parent) ` + -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..eb2d0c13 --- /dev/null +++ b/scripts/tests/plugins/Generate-Plugins.Tests.ps1 @@ -0,0 +1,321 @@ +#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' { + $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' + } +} 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 + } +}