diff --git a/.codespellrc b/.codespellrc index 1a42b532..d38ea35d 100644 --- a/.codespellrc +++ b/.codespellrc @@ -45,7 +45,9 @@ # Wee, Sherif - proper name (Wee, Sherif, contributor names should not be flagged as typos) # queston - intentional misspelling example in skills/arize-dataset/SKILL.md demonstrating typo detection in field names - + +# extenions - intentional misspelled key name used in plugin validators to detect invalid manifests + # nin - MongoDB $nin operator in security instructions NoSQL injection detection regex # Vertexes - FreeCAD shape sub-elements used as property of obj.Shape @@ -60,7 +62,7 @@ # Vally/vally - Name of product -ignore-words-list = numer,wit,aks,edn,ser,ois,gir,rouge,categor,aline,ative,afterall,deques,dateA,dateB,TE,FillIn,alle,vai,LOD,InOut,pixelX,aNULL,Wee,Sherif,queston,Vertexes,nin,FO,CAF,Parth,ans,gud,Vally,vally +ignore-words-list = numer,wit,aks,edn,ser,ois,gir,rouge,categor,aline,ative,afterall,deques,dateA,dateB,TE,FillIn,alle,vai,LOD,InOut,pixelX,aNULL,Wee,Sherif,queston,extenions,Vertexes,nin,FO,CAF,Parth,ans,gud,Vally,vally # Skip certain files and directories # *.tm7 - MTM DataContract exports; embedded base64 icon blobs trigger false positives diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 03689693..991790aa 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "accessibility-kanban", "source": "extensions/accessibility-kanban", "description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "acreadiness-cockpit", @@ -85,13 +85,13 @@ "name": "apng-studio", "source": "extensions/apng-studio", "description": "Interactive GitHub Copilot app canvas extension for building Animated PNG (APNG) files from frames. Draw or upload frames, tune per-frame timing and compositing, preview live, send the result to your phone by QR, and export an animated .png.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "arcade-canvas", "source": "extensions/arcade-canvas", "description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "arch", @@ -158,7 +158,7 @@ "name": "backlog-swipe-triage", "source": "extensions/backlog-swipe-triage", "description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "cast-imaging", @@ -195,7 +195,7 @@ "name": "chromium-control-canvas", "source": "extensions/chromium-control-canvas", "description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "clojure-interactive-programming", @@ -239,13 +239,13 @@ "name": "color-orb", "source": "extensions/color-orb", "description": "A visual orb that users can ask the agent to recolor while showing a live activity log in the canvas.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "connector-namespaces", "source": "extensions/connector-namespaces", "description": "Browse, connect, and open MCP connectors from an Azure Connector Namespace.", - "version": "1.1.0" + "version": "1.1.1" }, { "name": "context-engineering", @@ -373,7 +373,7 @@ "name": "diagram-viewer", "source": "extensions/diagram-viewer", "description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "dotnet", @@ -520,7 +520,7 @@ "name": "feedback-themes", "source": "extensions/feedback-themes", "description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "figma", @@ -593,7 +593,7 @@ "name": "gesture-review", "source": "extensions/gesture-review", "description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "gh-skills-builder", @@ -701,7 +701,7 @@ "name": "java-modernization-studio", "source": "extensions/java-modernization-studio", "description": "Drive the GitHub Copilot App Modernization for Java workflow from an interactive canvas: environment readiness, repo assessment, prioritized plan and progress, validation gates, and one-click predefined-task runs grounded in the repo's real artifacts.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "kotlin-mcp-development", @@ -980,13 +980,13 @@ "name": "release-notes-showcase", "source": "extensions/release-notes-showcase", "description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "repo-actions-hub", "source": "extensions/repo-actions-hub", "description": "Browse repository GitHub Actions workflows, inspect recent runs, and trigger manual workflow_dispatch runs from a Copilot canvas.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "roundup", @@ -1028,7 +1028,7 @@ "name": "site-studio", "source": "extensions/site-studio", "description": "Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "skill-image-gen", @@ -1118,13 +1118,13 @@ "name": "tiny-tool-town-submitter", "source": "extensions/tiny-tool-town-submitter", "description": "Inspect a repository, improve Tiny Tool Town readiness, submit its listing issue, and launch remediation work.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "token-pacman", "source": "extensions/token-pacman", "description": "Visualizes live session AI-credit usage as a Pac-Man board with pellets, ghosts, fruit milestones, and game-over limits.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "typescript-mcp-development", @@ -1311,7 +1311,7 @@ "name": "where-was-i", "source": "extensions/where-was-i", "description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.", - "version": "1.0.0" + "version": "1.0.1" }, { "name": "winappcli", @@ -1375,7 +1375,7 @@ "name": "work-hub", "source": "extensions/work-hub", "description": "Generic cross-repo command center canvas for GitHub Copilot with onboarding, focus planning, repo health, work signals, and session cleanup.", - "version": "1.0.0" + "version": "1.0.1" } ] } diff --git a/.github/workflows/external-plugin-command-router.yml b/.github/workflows/external-plugin-command-router.yml index 1b8c317a..5769f3b5 100644 --- a/.github/workflows/external-plugin-command-router.yml +++ b/.github/workflows/external-plugin-command-router.yml @@ -753,6 +753,7 @@ jobs: vally_lint_status: 'infra_error', smoke_status: 'infra_error', version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', failure_class: 'infra', summary: 'Quality-gate workflow failed unexpectedly. Re-run intake to retry.', }; @@ -764,6 +765,7 @@ jobs: vally_lint_status: 'infra_error', smoke_status: 'infra_error', version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', failure_class: 'infra', summary: 'Quality-gate workflow did not return results. Re-run intake to retry.', }; diff --git a/.github/workflows/external-plugin-intake.yml b/.github/workflows/external-plugin-intake.yml index c3859a31..ca7852dd 100644 --- a/.github/workflows/external-plugin-intake.yml +++ b/.github/workflows/external-plugin-intake.yml @@ -116,6 +116,7 @@ jobs: vally_lint_status: 'infra_error', smoke_status: 'infra_error', version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', failure_class: 'infra', summary: 'Quality-gate workflow failed unexpectedly. Re-run intake to retry.', }; @@ -127,6 +128,7 @@ jobs: vally_lint_status: 'infra_error', smoke_status: 'infra_error', version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', failure_class: 'infra', summary: 'Quality-gate workflow did not return results. Re-run intake to retry.', }; diff --git a/.github/workflows/external-plugin-pr-quality-gates.yml b/.github/workflows/external-plugin-pr-quality-gates.yml index 807cb27d..3010788b 100644 --- a/.github/workflows/external-plugin-pr-quality-gates.yml +++ b/.github/workflows/external-plugin-pr-quality-gates.yml @@ -158,6 +158,7 @@ jobs: failure_class: 'infra', checked_plugins: [], version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', summary: 'External plugin PR change detection failed unexpectedly. Re-run this workflow.', }; } else if (shouldRun) { @@ -167,6 +168,7 @@ jobs: failure_class: 'infra', checked_plugins: [], version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', summary: 'External plugin PR quality checks failed unexpectedly. Re-run this workflow.', }; } else if (process.env.QUALITY_RESULT_JSON) { @@ -177,6 +179,7 @@ jobs: failure_class: 'infra', checked_plugins: [], version_match_status: 'infra_error', + canvas_structure_status: 'infra_error', summary: 'External plugin PR quality checks did not return a result payload.', }; } @@ -245,15 +248,16 @@ jobs: const sourceUrl = String(entry?.source_tree_url || ''); const locator = String(entry?.source?.sha || entry?.source?.ref || 'repository'); const sourceCell = sourceUrl ? `[${locator}](${sourceUrl})` : locator; - return `| ${name} | ${quality.vally_lint_status || 'not_run'} | ${quality.smoke_status || 'not_run'} | ${quality.version_match_status || 'not_run'} | ${quality.overall_status || 'not_run'} | ${sourceCell} |`; + return `| ${name} | ${quality.vally_lint_status || 'not_run'} | ${quality.smoke_status || 'not_run'} | ${quality.version_match_status || 'not_run'} | ${quality.canvas_structure_status || 'not_run'} | ${quality.overall_status || 'not_run'} | ${sourceCell} |`; }) - : ['| _none_ | not_run | not_run | not_run | not_run | _n/a_ |']; + : ['| _none_ | not_run | not_run | not_run | not_run | not_run | _n/a_ |']; const failureDetails = checkedPlugins.flatMap((entry) => { const name = String(entry?.name || 'unknown'); const quality = entry?.quality || {}; const shouldShowVally = quality.vally_lint_status === 'fail' || quality.vally_lint_status === 'infra_error' || String(quality.vally_lint_output || '').trim().length > 0; const shouldShowSmoke = quality.smoke_status === 'fail' || quality.smoke_status === 'infra_error' || String(quality.smoke_output || '').trim().length > 0; const shouldShowVersionMatch = quality.version_match_status === 'fail' || quality.version_match_status === 'infra_error' || String(quality.version_match_output || '').trim().length > 0; + const shouldShowCanvasStructure = quality.canvas_structure_status === 'fail' || quality.canvas_structure_status === 'infra_error' || String(quality.canvas_structure_output || '').trim().length > 0; const details = []; if (shouldShowVally) { @@ -265,6 +269,9 @@ jobs: if (shouldShowVersionMatch) { details.push(formatGateOutput(name, 'version match', quality.version_match_status, quality.version_match_output)); } + if (shouldShowCanvasStructure) { + details.push(formatGateOutput(name, 'canvas structure', quality.canvas_structure_status, quality.canvas_structure_output)); + } return details; }); @@ -277,8 +284,8 @@ jobs: '', '### Per-plugin quality summary', '', - '| Plugin | vally lint | install smoke test | version match | overall | source tree |', - '|---|---|---|---|---|---|', + '| Plugin | vally lint | install smoke test | version match | canvas structure | overall | source tree |', + '|---|---|---|---|---|---|---|', ...rows, '', ...(failureDetails.length > 0 diff --git a/AGENTS.md b/AGENTS.md index 7eedca71..3a17693c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,7 +89,7 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must - Extension `plugin.json` **must** follow the convention: - `name`, `description`, `version` are required - `logo` **must** be exactly `"assets/preview.png"` (enforced convention) - - `extensions` **must** be exactly `"."` (per [copilot-agent-runtime#9929](https://github.com/github/copilot-agent-runtime/pull/9929)) + - `extensions` **must** be exactly `"."` in source manifests (materialization rewrites this to `"extensions"` for distribution output) - Optional: `author`, `keywords` fields - **Must not** include `x-awesome-copilot` field (use convention-based `assets/preview.png` only) - Each extension must have `assets/preview.png` as the primary visual asset diff --git a/eng/clean-materialized-plugins.mjs b/eng/clean-materialized-plugins.mjs index 9379d78a..2fcbacc2 100644 --- a/eng/clean-materialized-plugins.mjs +++ b/eng/clean-materialized-plugins.mjs @@ -6,6 +6,7 @@ import { fileURLToPath } from "url"; import { ROOT_FOLDER } from "./constants.mjs"; const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins"); +const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions"); const MATERIALIZED_SPECS = { agents: { path: "agents", @@ -89,6 +90,30 @@ function cleanPlugin(pluginPath) { return { removed, manifestUpdated }; } +function cleanMaterializedExtensionPlugin(extensionPath) { + const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json"); + let manifestUpdated = false; + if (fs.existsSync(pluginJsonPath)) { + const plugin = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8")); + if (plugin.extensions === "extensions") { + plugin.extensions = "."; + fs.writeFileSync(pluginJsonPath, JSON.stringify(plugin, null, 2) + "\n", "utf8"); + manifestUpdated = true; + console.log(` Updated ${path.basename(extensionPath)}/.github/plugin/plugin.json`); + } + } + + const target = path.join(extensionPath, "extensions"); + if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) { + return { removed: 0, manifestUpdated }; + } + + const count = countFiles(target); + fs.rmSync(target, { recursive: true, force: true }); + console.log(` Removed ${path.basename(extensionPath)}/extensions/ (${count} files)`); + return { removed: count, manifestUpdated }; +} + function countFiles(dir) { let count = 0; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { @@ -171,6 +196,25 @@ function main() { } } + if (fs.existsSync(EXTENSIONS_DIR)) { + const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + + for (const dirName of extensionDirs) { + const extensionPath = path.join(EXTENSIONS_DIR, dirName); + if (!fs.existsSync(path.join(extensionPath, "extension.mjs"))) { + continue; + } + const { removed, manifestUpdated } = cleanMaterializedExtensionPlugin(extensionPath); + total += removed; + if (manifestUpdated) { + manifestsUpdated++; + } + } + } + console.log(); if (total === 0 && manifestsUpdated === 0) { console.log("✅ No materialized files found. Plugins are already clean."); diff --git a/eng/external-plugin-intake.mjs b/eng/external-plugin-intake.mjs index 794e36f5..fa9696e1 100644 --- a/eng/external-plugin-intake.mjs +++ b/eng/external-plugin-intake.mjs @@ -459,6 +459,55 @@ async function validateCanvasPluginMetadata(plugin, errors, warnings, token) { ); } + if (manifest.extenions !== undefined) { + errors.push( + `submission: plugins tagged with "canvas" must use "extensions" (found misspelled key "extenions") in "${manifestPath}"`, + ); + } + + if (manifest.extensions !== undefined && manifest.extensions !== "extensions") { + errors.push( + `submission: plugins tagged with "canvas" may omit "extensions", but if provided it must be "extensions" in "${manifestPath}"`, + ); + } + + const extensionContainerPath = joinRepoPath(pluginRoot, "extensions"); + const extensionContainerResponse = await fetchGitHubFile(repo, extensionContainerPath, releaseLocator, token); + if (extensionContainerResponse.kind === "notFound") { + errors.push( + `submission: plugins tagged with "canvas" must include an "extensions" directory at ${releaseLocatorDescription}`, + ); + } else if (extensionContainerResponse.kind === "apiError") { + warnings.push( + `submission: could not verify "extensions" directory in GitHub repository "${repo}" at ${releaseLocatorDescription}; a maintainer should re-run intake`, + ); + } else if ( + !( + extensionContainerResponse.data?.type === "dir" + || Array.isArray(extensionContainerResponse.data) + ) + ) { + errors.push( + `submission: "extensions" must be a directory in ${releaseLocatorDescription}`, + ); + } + + const extensionEntryPath = joinRepoPath(pluginRoot, "extensions", "extension.mjs"); + const extensionEntryResponse = await fetchGitHubFile(repo, extensionEntryPath, releaseLocator, token); + if (extensionEntryResponse.kind === "notFound") { + errors.push( + `submission: plugins tagged with "canvas" must include "extensions/extension.mjs" at ${releaseLocatorDescription}`, + ); + } else if (extensionEntryResponse.kind === "apiError") { + warnings.push( + `submission: could not verify "extensions/extension.mjs" in GitHub repository "${repo}" at ${releaseLocatorDescription}; a maintainer should re-run intake`, + ); + } else if (extensionEntryResponse.data?.type !== "file") { + errors.push( + `submission: "extensions/extension.mjs" must be a file in ${releaseLocatorDescription}`, + ); + } + const previewPath = joinRepoPath(pluginRoot, EXTERNAL_CANVAS_PREVIEW_PATH); const previewResponse = await fetchGitHubFile(repo, previewPath, releaseLocator, token); if (previewResponse.kind === "notFound") { @@ -580,11 +629,13 @@ function normalizeQualityGateResult(rawResult) { vally_lint_status: "not_run", smoke_status: "not_run", version_match_status: "not_run", + canvas_structure_status: "not_run", failure_class: "none", summary: "", vally_lint_output: "", smoke_output: "", version_match_output: "", + canvas_structure_output: "", }; if (!rawResult || typeof rawResult !== "object" || Array.isArray(rawResult)) { @@ -601,6 +652,7 @@ function buildQualityGatesCommentSection(qualityResult) { const vallyState = qualityResult.vally_lint_status || "not_run"; const smokeState = qualityResult.smoke_status || "not_run"; const versionMatchState = qualityResult.version_match_status || "not_run"; + const canvasStructureState = qualityResult.canvas_structure_status || "not_run"; const summaryText = String(qualityResult.summary || "").trim() || "_No quality gate details were provided._"; const sections = [ @@ -611,6 +663,7 @@ function buildQualityGatesCommentSection(qualityResult) { `| vally lint | ${vallyState} |`, `| install smoke test | ${smokeState} |`, `| version match | ${versionMatchState} |`, + `| canvas structure | ${canvasStructureState} |`, "", summaryText, ]; @@ -660,6 +713,21 @@ function buildQualityGatesCommentSection(qualityResult) { ); } + const canvasStructureOutput = String(qualityResult.canvas_structure_output || "").trim(); + if (canvasStructureOutput) { + sections.push( + "", + "
", + "Canvas structure output", + "", + "```text", + canvasStructureOutput, + "```", + "", + "
", + ); + } + return sections.join("\n"); } diff --git a/eng/external-plugin-pr-quality-gates.mjs b/eng/external-plugin-pr-quality-gates.mjs index 5cbf4c9b..85cd6d9c 100644 --- a/eng/external-plugin-pr-quality-gates.mjs +++ b/eng/external-plugin-pr-quality-gates.mjs @@ -86,7 +86,7 @@ export async function runExternalPluginPrQualityGates(plugins) { ? "No changed external plugin entries were detected in plugins/external.json." : checkedPlugins .map((entry) => - `- ${entry.name}: vally-lint=${entry.quality.vally_lint_status}, install-smoke=${entry.quality.smoke_status}, version-match=${entry.quality.version_match_status}, overall=${entry.quality.overall_status}` + `- ${entry.name}: vally-lint=${entry.quality.vally_lint_status}, install-smoke=${entry.quality.smoke_status}, version-match=${entry.quality.version_match_status}, canvas-structure=${entry.quality.canvas_structure_status}, overall=${entry.quality.overall_status}` ) .join("\n"); diff --git a/eng/external-plugin-quality-gates.mjs b/eng/external-plugin-quality-gates.mjs index 358ecdda..507cc28f 100644 --- a/eng/external-plugin-quality-gates.mjs +++ b/eng/external-plugin-quality-gates.mjs @@ -8,6 +8,7 @@ import { spawnSync } from "child_process"; import { runLint, LintConsoleReporter } from "@microsoft/vally"; const MAX_OUTPUT_LENGTH = 12000; +const EXTERNAL_CANVAS_KEYWORD = "canvas"; const INFRA_ERROR_PATTERNS = [ /\b401\b/, @@ -69,6 +70,12 @@ function normalizePluginPath(pluginPath) { return normalized; } +function hasCanvasKeyword(plugin) { + return (plugin?.keywords ?? []).some( + (keyword) => String(keyword).trim().toLowerCase() === EXTERNAL_CANVAS_KEYWORD, + ); +} + function resolveFetchSpec(pluginSource) { if (pluginSource.sha) { return pluginSource.sha; @@ -467,6 +474,148 @@ function runVersionMatchGate(repoDir, plugin, primaryFetchSpec) { }; } +function checkPathExistsAtLocator(repoDir, locator, repoPath, expectedType) { + const result = runCommand("git", ["cat-file", "-e", `${locator}:${repoPath}`], { cwd: repoDir }); + if (result.exitCode === 0) { + if (!expectedType) { + return { exists: true, output: "" }; + } + + const typeResult = runCommand("git", ["cat-file", "-t", `${locator}:${repoPath}`], { cwd: repoDir }); + if (typeResult.exitCode !== 0) { + return { + exists: false, + output: `Unable to verify path "${repoPath}" type at "${locator}": ${typeResult.output}`, + }; + } + + const actualType = String(typeResult.stdout ?? "").trim(); + if (actualType !== expectedType) { + return { + exists: false, + output: "", + kindMismatch: true, + actualType, + }; + } + + return { exists: true, output: "" }; + } + + const normalizedOutput = String(result.output ?? "").toLowerCase(); + if ( + normalizedOutput.includes("not a valid object name") + || normalizedOutput.includes("path '") + || normalizedOutput.includes("does not exist") + ) { + return { exists: false, output: "" }; + } + + return { + exists: false, + output: `Unable to verify path "${repoPath}" at "${locator}": ${result.output}`, + }; +} + +export function runCanvasStructureGate(repoDir, plugin, primaryFetchSpec) { + if (!hasCanvasKeyword(plugin)) { + return { + status: "not_run", + output: "Canvas structure gate skipped because plugin is not tagged with \"canvas\".", + }; + } + + const normalizedPluginPath = normalizePluginPath(plugin?.source?.path || "/"); + const locators = [plugin?.source?.ref, plugin?.source?.sha] + .filter((value) => typeof value === "string" && value.trim().length > 0) + .map((value) => value.trim()) + .filter((value, index, values) => values.indexOf(value) === index); + + if (locators.length === 0) { + return { + status: "not_run", + output: "Canvas structure gate skipped because neither source.ref nor source.sha was provided.", + }; + } + + const extensionsDir = toPosixPath(normalizedPluginPath, "extensions"); + const extensionEntryPoint = toPosixPath(extensionsDir, "extension.mjs"); + + let hasFailure = false; + let hasInfraError = false; + const messages = []; + + for (const locator of locators) { + if (locator !== primaryFetchSpec) { + const fetchResult = fetchLocatorIntoRepo(repoDir, locator); + if (fetchResult.status === "fail") { + hasFailure = true; + messages.push(`- ${locator}: ${fetchResult.output}`); + continue; + } + + if (fetchResult.status === "infra_error") { + hasInfraError = true; + messages.push(`- ${locator}: ${fetchResult.output}`); + continue; + } + } + + const extensionDirCheck = checkPathExistsAtLocator(repoDir, locator, extensionsDir, "tree"); + if (extensionDirCheck.output) { + hasInfraError = true; + messages.push(`- ${locator}: ${extensionDirCheck.output}`); + continue; + } + if (!extensionDirCheck.exists) { + hasFailure = true; + if (extensionDirCheck.kindMismatch) { + messages.push(`- ${locator}: "${extensionsDir}" must be a directory.`); + } else { + messages.push(`- ${locator}: missing required canvas extension directory "${extensionsDir}".`); + } + continue; + } + + const extensionEntryCheck = checkPathExistsAtLocator(repoDir, locator, extensionEntryPoint, "blob"); + if (extensionEntryCheck.output) { + hasInfraError = true; + messages.push(`- ${locator}: ${extensionEntryCheck.output}`); + continue; + } + if (!extensionEntryCheck.exists) { + hasFailure = true; + if (extensionEntryCheck.kindMismatch) { + messages.push(`- ${locator}: "${extensionEntryPoint}" must be a file.`); + } else { + messages.push(`- ${locator}: missing required canvas extension entry point "${extensionEntryPoint}".`); + } + continue; + } + + messages.push(`- ${locator}: found "${extensionsDir}" with entry point "${extensionEntryPoint}".`); + } + + if (hasInfraError) { + return { + status: "infra_error", + output: messages.join("\n"), + }; + } + + if (hasFailure) { + return { + status: "fail", + output: messages.join("\n"), + }; + } + + return { + status: "pass", + output: messages.join("\n"), + }; +} + function toOverallStatus(states) { if (states.includes("infra_error")) { return "infra_error"; @@ -497,11 +646,13 @@ export async function runExternalPluginQualityGates(plugin) { vally_lint_status: "not_run", smoke_status: "not_run", version_match_status: "not_run", + canvas_structure_status: "not_run", failure_class: "none", summary: "", vally_lint_output: "", smoke_output: "", version_match_output: "", + canvas_structure_output: "", }; try { @@ -513,10 +664,14 @@ export async function runExternalPluginQualityGates(plugin) { result.vally_lint_status = "fail"; result.smoke_status = "fail"; result.version_match_status = "fail"; + result.canvas_structure_status = hasCanvasKeyword(plugin) ? "fail" : "not_run"; result.overall_status = "fail"; result.failure_class = "submitter_fixes"; result.summary = `Plugin path "${plugin.source?.path || "/"}" was not found in the submitted repository snapshot.`; result.version_match_output = result.summary; + if (hasCanvasKeyword(plugin)) { + result.canvas_structure_output = result.summary; + } return result; } @@ -524,6 +679,10 @@ export async function runExternalPluginQualityGates(plugin) { result.version_match_status = versionMatchResult.status; result.version_match_output = versionMatchResult.output; + const canvasStructureResult = runCanvasStructureGate(repoDir, plugin, fetchSpec); + result.canvas_structure_status = canvasStructureResult.status; + result.canvas_structure_output = canvasStructureResult.output; + const vallyResult = await runVallyLintGate(pluginRoot); result.vally_lint_status = vallyResult.status; result.vally_lint_output = vallyResult.output; @@ -532,12 +691,18 @@ export async function runExternalPluginQualityGates(plugin) { result.smoke_status = smokeResult.status; result.smoke_output = smokeResult.output; - result.overall_status = toOverallStatus([result.vally_lint_status, result.smoke_status, result.version_match_status]); + result.overall_status = toOverallStatus([ + result.vally_lint_status, + result.smoke_status, + result.version_match_status, + result.canvas_structure_status, + ]); result.failure_class = toFailureClass(result.overall_status); result.summary = [ `- vally lint: ${result.vally_lint_status}`, `- install smoke test: ${result.smoke_status}`, `- version match: ${result.version_match_status}`, + `- canvas structure: ${result.canvas_structure_status}`, `- overall: ${result.overall_status}`, ].join("\n"); diff --git a/eng/external-plugin-quality-gates.test.mjs b/eng/external-plugin-quality-gates.test.mjs new file mode 100644 index 00000000..5f619fae --- /dev/null +++ b/eng/external-plugin-quality-gates.test.mjs @@ -0,0 +1,101 @@ +import assert from "node:assert/strict"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import { spawnSync } from "child_process"; +import { after, test } from "node:test"; +import { runCanvasStructureGate } from "./external-plugin-quality-gates.mjs"; + +const tempDirs = []; + +after(() => { + for (const dir of tempDirs) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +function runGit(repoDir, ...args) { + const result = spawnSync("git", args, { cwd: repoDir, encoding: "utf8" }); + if (result.status !== 0) { + throw new Error(`git ${args.join(" ")} failed: ${result.stdout}\n${result.stderr}`); + } + return String(result.stdout ?? "").trim(); +} + +function createTempRepo() { + const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), "external-plugin-quality-")); + tempDirs.push(repoDir); + + runGit(repoDir, "init", "-q"); + runGit(repoDir, "config", "user.name", "Copilot Test"); + runGit(repoDir, "config", "user.email", "copilot@example.com"); + return repoDir; +} + +function commitAll(repoDir, message) { + runGit(repoDir, "add", "-A"); + runGit(repoDir, "commit", "-m", message, "--quiet"); + return runGit(repoDir, "rev-parse", "HEAD"); +} + +test("runCanvasStructureGate passes when extensions/extension.mjs exists", () => { + const repoDir = createTempRepo(); + fs.mkdirSync(path.join(repoDir, "extensions"), { recursive: true }); + fs.writeFileSync(path.join(repoDir, "extensions", "extension.mjs"), "export default {};\n"); + const sha = commitAll(repoDir, "Add canvas extension container"); + + const plugin = { + name: "canvas-plugin", + keywords: ["canvas"], + source: { + source: "github", + repo: "owner/repo", + sha, + }, + }; + + const result = runCanvasStructureGate(repoDir, plugin, sha); + assert.equal(result.status, "pass"); + assert.match(result.output, /found "extensions"/); +}); + +test("runCanvasStructureGate fails when extension entrypoint is only at repo root", () => { + const repoDir = createTempRepo(); + fs.writeFileSync(path.join(repoDir, "extension.mjs"), "export default {};\n"); + const sha = commitAll(repoDir, "Add root extension entrypoint"); + + const plugin = { + name: "canvas-plugin", + keywords: ["canvas"], + source: { + source: "github", + repo: "owner/repo", + sha, + }, + }; + + const result = runCanvasStructureGate(repoDir, plugin, sha); + assert.equal(result.status, "fail"); + assert.match(result.output, /missing required canvas extension directory "extensions"/); +}); + +test("runCanvasStructureGate fails when extension entrypoint path is a directory", () => { + const repoDir = createTempRepo(); + fs.mkdirSync(path.join(repoDir, "extensions", "extension.mjs"), { recursive: true }); + fs.writeFileSync(path.join(repoDir, "extensions", "extension.mjs", "placeholder.txt"), "not-a-module\n"); + const sha = commitAll(repoDir, "Add invalid extension entrypoint directory"); + + const plugin = { + name: "canvas-plugin", + keywords: ["canvas"], + source: { + source: "github", + repo: "owner/repo", + sha, + }, + }; + + const result = runCanvasStructureGate(repoDir, plugin, sha); + assert.equal(result.status, "fail"); + assert.match(result.output, /"extensions\/extension\.mjs" must be a file/); +}); diff --git a/eng/materialize-plugins.mjs b/eng/materialize-plugins.mjs index 978c250f..2d91cb61 100644 --- a/eng/materialize-plugins.mjs +++ b/eng/materialize-plugins.mjs @@ -2,9 +2,11 @@ import fs from "fs"; import path from "path"; +import { fileURLToPath } from "url"; import { ROOT_FOLDER } from "./constants.mjs"; const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins"); +const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions"); /** * Recursively copy a directory. @@ -22,6 +24,17 @@ function copyDirRecursive(src, dest) { } } +function copyEntryRecursive(srcPath, destPath) { + const stats = fs.statSync(srcPath); + if (stats.isDirectory()) { + copyDirRecursive(srcPath, destPath); + return; + } + + fs.mkdirSync(path.dirname(destPath), { recursive: true }); + fs.copyFileSync(srcPath, destPath); +} + /** * Resolve a plugin-relative path to the repo-root source file. * @@ -45,6 +58,48 @@ function resolveSource(relPath) { return null; } +export function materializeExtensionPlugin(extensionPath) { + const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json"); + if (!fs.existsSync(pluginJsonPath)) { + return { copiedEntries: 0, manifestUpdated: false, skipped: true }; + } + + let metadata; + try { + metadata = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8")); + } catch (err) { + throw new Error(`Failed to parse ${pluginJsonPath}: ${err.message}`); + } + + const extensionContainerPath = path.join(extensionPath, "extensions"); + fs.rmSync(extensionContainerPath, { recursive: true, force: true }); + fs.mkdirSync(extensionContainerPath, { recursive: true }); + + let copiedEntries = 0; + for (const entry of fs.readdirSync(extensionPath, { withFileTypes: true })) { + if (entry.name === ".github" || entry.name === "extensions") { + continue; + } + + copyEntryRecursive( + path.join(extensionPath, entry.name), + path.join(extensionContainerPath, entry.name) + ); + copiedEntries++; + } + + let manifestUpdated = false; + if (metadata.extensions !== "extensions") { + metadata.extensions = "extensions"; + manifestUpdated = true; + } + if (manifestUpdated) { + fs.writeFileSync(pluginJsonPath, JSON.stringify(metadata, null, 2) + "\n", "utf8"); + } + + return { copiedEntries, manifestUpdated, skipped: false }; +} + function materializePlugins() { console.log("Materializing plugin files...\n"); @@ -61,6 +116,8 @@ function materializePlugins() { let totalAgents = 0; let totalSkills = 0; let totalExtensions = 0; + let totalExtensionPlugins = 0; + let totalExtensionPluginEntries = 0; let warnings = 0; let errors = 0; @@ -185,7 +242,36 @@ function materializePlugins() { } } - console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} extensions.`); + if (fs.existsSync(EXTENSIONS_DIR)) { + const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + + for (const dirName of extensionDirs) { + const extensionPath = path.join(EXTENSIONS_DIR, dirName); + if (!fs.existsSync(path.join(extensionPath, "extension.mjs"))) { + continue; + } + + try { + const result = materializeExtensionPlugin(extensionPath); + if (result.skipped) { + continue; + } + + totalExtensionPlugins++; + totalExtensionPluginEntries += result.copiedEntries; + console.log(`✓ ${dirName}: materialized extension bundle into ./extensions (${result.copiedEntries} entries)`); + } catch (err) { + console.error(`Error: Failed to materialize extension plugin ${dirName}: ${err.message}`); + errors++; + } + } + } + + console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} plugin extension refs.`); + console.log(`Materialized ${totalExtensionPlugins} extension plugins (${totalExtensionPluginEntries} top-level entries).`); if (warnings > 0) { console.log(`${warnings} warning(s).`); } @@ -195,4 +281,8 @@ function materializePlugins() { } } -materializePlugins(); +export { materializePlugins }; + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + materializePlugins(); +} diff --git a/eng/materialize-plugins.test.mjs b/eng/materialize-plugins.test.mjs new file mode 100644 index 00000000..7f42981c --- /dev/null +++ b/eng/materialize-plugins.test.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import { after, test } from "node:test"; +import { materializeExtensionPlugin } from "./materialize-plugins.mjs"; + +const tempDirs = []; + +after(() => { + for (const dir of tempDirs) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +test("materializeExtensionPlugin writes extension bundles to ./extensions and rewrites manifest", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "materialize-extension-plugin-")); + tempDirs.push(tempDir); + + const pluginDir = path.join(tempDir, "extension-plugin"); + fs.mkdirSync(path.join(pluginDir, ".github", "plugin"), { recursive: true }); + fs.mkdirSync(path.join(pluginDir, "assets"), { recursive: true }); + fs.writeFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), JSON.stringify({ + name: "test-extension-plugin", + description: "test plugin", + version: "1.0.0", + logo: "assets/preview.png", + extensions: ".", + }, null, 2)); + fs.writeFileSync(path.join(pluginDir, "extension.mjs"), "export default {};\n"); + fs.writeFileSync(path.join(pluginDir, "README.md"), "# test\n"); + fs.writeFileSync(path.join(pluginDir, "assets", "preview.png"), "fake-image-bytes"); + + const result = materializeExtensionPlugin(pluginDir); + + assert.equal(result.skipped, false); + assert.equal(result.manifestUpdated, true); + assert.equal(result.copiedEntries, 3); + assert.equal(fs.existsSync(path.join(pluginDir, "extensions", "extension.mjs")), true); + assert.equal(fs.existsSync(path.join(pluginDir, "extensions", "assets", "preview.png")), true); + assert.equal(fs.existsSync(path.join(pluginDir, "extensions", "README.md")), true); + assert.equal(fs.existsSync(path.join(pluginDir, "extensions", ".github")), false); + + const pluginManifest = JSON.parse( + fs.readFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), "utf8") + ); + assert.equal(pluginManifest.extensions, "extensions"); +}); diff --git a/eng/validate-plugins.mjs b/eng/validate-plugins.mjs index fc8003b3..7161df06 100755 --- a/eng/validate-plugins.mjs +++ b/eng/validate-plugins.mjs @@ -305,9 +305,14 @@ function validateExtensionManifest(folderName) { errors.push("x-awesome-copilot field must not be present (use convention-based logo instead)"); } - // Extension convention: extensions field must be "." + if (parsed.extenions !== undefined) { + errors.push('use "extensions" field (found misspelled key "extenions")'); + } + + // Extension convention: source manifests keep extensions at repository root. + // Materialization rewrites this to "extensions" on distribution branches. if (parsed.extensions !== ".") { - errors.push('extensions field must be exactly "." (extension convention)'); + errors.push('extensions field must be exactly "." in source manifests (extension convention)'); } return { errors, plugin: parsedPlugin }; diff --git a/extensions/accessibility-kanban/.github/plugin/plugin.json b/extensions/accessibility-kanban/.github/plugin/plugin.json index 824a71c5..03903a27 100644 --- a/extensions/accessibility-kanban/.github/plugin/plugin.json +++ b/extensions/accessibility-kanban/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "accessibility-kanban", "description": "Kanban board to manage accessibility issues, allow you to plan, track, and complete remediation work.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/apng-studio/.github/plugin/plugin.json b/extensions/apng-studio/.github/plugin/plugin.json index 6c46943c..fcd5bfc8 100644 --- a/extensions/apng-studio/.github/plugin/plugin.json +++ b/extensions/apng-studio/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "apng-studio", "description": "Interactive GitHub Copilot app canvas extension for building Animated PNG (APNG) files from frames. Draw or upload frames, tune per-frame timing and compositing, preview live, send the result to your phone by QR, and export an animated .png.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Andrea Griffiths", "url": "https://github.com/AndreaGriffiths11" diff --git a/extensions/arcade-canvas/.github/plugin/plugin.json b/extensions/arcade-canvas/.github/plugin/plugin.json index 8bf1d4cb..975971d8 100644 --- a/extensions/arcade-canvas/.github/plugin/plugin.json +++ b/extensions/arcade-canvas/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "arcade-canvas", "description": "Play five retro Phaser mini-games in a Copilot canvas while agents work.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Dan Wahlin", "url": "https://github.com/DanWahlin" diff --git a/extensions/backlog-swipe-triage/.github/plugin/plugin.json b/extensions/backlog-swipe-triage/.github/plugin/plugin.json index d45e7e39..6dda88bd 100644 --- a/extensions/backlog-swipe-triage/.github/plugin/plugin.json +++ b/extensions/backlog-swipe-triage/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "backlog-swipe-triage", "description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" diff --git a/extensions/chromium-control-canvas/.github/plugin/plugin.json b/extensions/chromium-control-canvas/.github/plugin/plugin.json index c8a2fa72..8c4d631d 100644 --- a/extensions/chromium-control-canvas/.github/plugin/plugin.json +++ b/extensions/chromium-control-canvas/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "chromium-control-canvas", "description": "Opens a real Chromium window you can navigate and interact with from a Copilot canvas control panel and agent actions.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Andrea Griffiths", "url": "https://github.com/AndreaGriffiths11" diff --git a/extensions/color-orb/.github/plugin/plugin.json b/extensions/color-orb/.github/plugin/plugin.json index c162c294..3b564245 100644 --- a/extensions/color-orb/.github/plugin/plugin.json +++ b/extensions/color-orb/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "color-orb", "description": "A visual orb that users can ask the agent to recolor while showing a live activity log in the canvas.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/connector-namespaces/.github/plugin/plugin.json b/extensions/connector-namespaces/.github/plugin/plugin.json index 18b8c116..397859c5 100644 --- a/extensions/connector-namespaces/.github/plugin/plugin.json +++ b/extensions/connector-namespaces/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "connector-namespaces", "description": "Browse, connect, and open MCP connectors from an Azure Connector Namespace.", - "version": "1.1.0", + "version": "1.1.1", "author": { "name": "Alex Yang", "url": "https://github.com/alexyaang" diff --git a/extensions/diagram-viewer/.github/plugin/plugin.json b/extensions/diagram-viewer/.github/plugin/plugin.json index 0ad6af5d..9e2b6ef2 100644 --- a/extensions/diagram-viewer/.github/plugin/plugin.json +++ b/extensions/diagram-viewer/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "diagram-viewer", "description": "Render diagrams, click nodes to drill down, and view agent-generated explanations directly in the canvas.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/feedback-themes/.github/plugin/plugin.json b/extensions/feedback-themes/.github/plugin/plugin.json index 3bbdbc96..09435104 100644 --- a/extensions/feedback-themes/.github/plugin/plugin.json +++ b/extensions/feedback-themes/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "feedback-themes", "description": "Explore grouped customer feedback signals by impact and drill into a theme to guide product next steps.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/gesture-review/.github/plugin/plugin.json b/extensions/gesture-review/.github/plugin/plugin.json index 648d069b..1ba5cf3b 100644 --- a/extensions/gesture-review/.github/plugin/plugin.json +++ b/extensions/gesture-review/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "gesture-review", "description": "Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/java-modernization-studio/.github/plugin/plugin.json b/extensions/java-modernization-studio/.github/plugin/plugin.json index 1f71b764..c3f22658 100644 --- a/extensions/java-modernization-studio/.github/plugin/plugin.json +++ b/extensions/java-modernization-studio/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "java-modernization-studio", "description": "Drive the GitHub Copilot App Modernization for Java workflow from an interactive canvas: environment readiness, repo assessment, prioritized plan and progress, validation gates, and one-click predefined-task runs grounded in the repo's real artifacts.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Ayan Gupta", "url": "https://github.com/ayangupt" diff --git a/extensions/release-notes-showcase/.github/plugin/plugin.json b/extensions/release-notes-showcase/.github/plugin/plugin.json index f87e6d7b..dcc4ba18 100644 --- a/extensions/release-notes-showcase/.github/plugin/plugin.json +++ b/extensions/release-notes-showcase/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "release-notes-showcase", "description": "Compose and refine launch-ready release notes with contributor callouts and export-friendly output.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Kayla Cinnamon", "url": "https://github.com/cinnamon-msft" diff --git a/extensions/repo-actions-hub/.github/plugin/plugin.json b/extensions/repo-actions-hub/.github/plugin/plugin.json index 24221451..bd317192 100644 --- a/extensions/repo-actions-hub/.github/plugin/plugin.json +++ b/extensions/repo-actions-hub/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "repo-actions-hub", "description": "Browse repository GitHub Actions workflows, inspect recent runs, and trigger manual workflow_dispatch runs from a Copilot canvas.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" diff --git a/extensions/site-studio/.github/plugin/plugin.json b/extensions/site-studio/.github/plugin/plugin.json index f2c294af..2c1605b6 100644 --- a/extensions/site-studio/.github/plugin/plugin.json +++ b/extensions/site-studio/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "site-studio", "description": "Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Ayan Gupta", "url": "https://github.com/ayangupt" diff --git a/extensions/tiny-tool-town-submitter/.github/plugin/plugin.json b/extensions/tiny-tool-town-submitter/.github/plugin/plugin.json index bdda3a08..70b8c98a 100644 --- a/extensions/tiny-tool-town-submitter/.github/plugin/plugin.json +++ b/extensions/tiny-tool-town-submitter/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "tiny-tool-town-submitter", "description": "Inspect a repository, improve Tiny Tool Town readiness, submit its listing issue, and launch remediation work.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" diff --git a/extensions/token-pacman/.github/plugin/plugin.json b/extensions/token-pacman/.github/plugin/plugin.json index 1bdc62c8..b7868bb1 100644 --- a/extensions/token-pacman/.github/plugin/plugin.json +++ b/extensions/token-pacman/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "token-pacman", "description": "Visualizes live session AI-credit usage as a Pac-Man board with pellets, ghosts, fruit milestones, and game-over limits.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno" diff --git a/extensions/where-was-i/.github/plugin/plugin.json b/extensions/where-was-i/.github/plugin/plugin.json index ca60cd3e..e43c6f26 100644 --- a/extensions/where-was-i/.github/plugin/plugin.json +++ b/extensions/where-was-i/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "where-was-i", "description": "Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Aaron Powell", "url": "https://github.com/aaronpowell" diff --git a/extensions/work-hub/.github/plugin/plugin.json b/extensions/work-hub/.github/plugin/plugin.json index 3e85d376..c3999519 100644 --- a/extensions/work-hub/.github/plugin/plugin.json +++ b/extensions/work-hub/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "work-hub", "description": "Generic cross-repo command center canvas for GitHub Copilot with onboarding, focus planning, repo health, work signals, and session cleanup.", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "James Montemagno", "url": "https://github.com/jamesmontemagno"