Add Agent Skill gallery acquisition and workspace import - #35
Conversation
Adds a new slash command and supporting Node script that let a developer acquire a Copilot Studio agent skill by either uploading a local SKILL.md/.zip or picking from the Power CAT cat-agent-skills gallery. - scripts/add-skill.js: zero-dependency Node script with 'list' (enumerate gallery submissions via the GitHub Trees API + raw metadata.json, filter to Copilot Studio, sort by name) and 'download' (materialize SKILL.md plus scripts/references/assets, and the prebuilt .zip when present). - commands/add-skill.md: /add-skill orchestration for source choice, listing, picking, downloading and reporting. Import into a Copilot Studio agent project is intentionally out of scope for this change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e8d082ed-ab52-4307-bdda-b4fb86c32cdd (cherry picked from commit dd017e0)
…sting Extend the /add-skill command beyond acquiring a skill so it can also materialize it into a cloned Copilot Studio agent workspace, and make the gallery easier to browse in the terminal. scripts/add-skill.js: - New `import --src <dir> --workspace <agentDir>` command that writes the skill under behaviors/<name>/: SKILL.md plus every payload file copied verbatim. By default it also emits portal-style .mcs.yml companions (an anchor skill.mcs.yml carrying the InlineAgentSkill identity, plus one <file>.mcs.yml sidecar per non-manifest file for bundle skills) so the on-disk layout matches a Copilot Studio portal import. Schema names are prefixed with the agent schemaName read from settings.mcs.yml; when the prefix is unavailable or --no-sidecars is passed, it falls back to a bare behaviors/ skill and warns. Supports --name, --force, and --include-sidecars, and surfaces non-fatal workspace checks as warnings. - `list --pretty` renders a deterministic, fixed-width box table (with a Slug column and bundle marker) instead of JSON. - `download --dest` is now optional and defaults to a temp folder (<os-tmp>/mcs-add-skill) so gallery downloads don't clutter the cwd. - Make skill sort order deterministic (code-point, slug tiebreaker). commands/add-skill.md: - Document the new acquire-then-import flow, the standalone source question, the --pretty table, temp-folder downloads, and the import step. Clarify that publishing to the cloud happens from the VS Code Copilot Studio extension - this command never pushes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 962abe8)
The skill schema was duplicated across three places that could drift: the copilot-studio-architect agent (inline variant), the /add-skill command (upload variant), and scripts/add-skill.js (the generator). Mirror what reference/knowledge-schema.md already does for knowledge sources and make a single file the source of truth. The two consumers now resolve the reference via the plugin root and follow it, keeping classification heuristics (skill vs tool, skill vs knowledge) in the architect where they belong. The inline-vs-upload selection rule is recorded as an open question rather than guessed at. Writing it down exposed three gaps in the generator, now fixed and covered by tests: - componentName bypassed yamlScalar on both the anchor and the sidecars, so a folder named "on" or "123" came back as a boolean or a number, and a payload path containing " #" was truncated at the YAML comment marker. yamlScalar also now quotes plain scalars that resolve to non-strings under YAML 1.1. - Component schema names had no 100-character Dataverse budget. Segments are truncated with a warning, and an agent schema name that leaves no room fails before any file is written instead of half-importing a skill. - main() ran on require, so the module could not be tested. Guard it with require.main and export the tested surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f42cd36d-11db-48e7-a770-959e3d0e40d7 (cherry picked from commit 56c0cef)
Fixes the actionable findings from the PR review bots: - readAgentSchemaPrefix now resolves the settings.mcs.yml value the way a YAML parser would. A quoted prefix used to be captured with its quotes and pasted straight into the anchor, producing both an invalid schema name and invalid YAML; a trailing inline comment used to defeat the match entirely and silently downgrade the import to a bare skill. The result is validated against the Dataverse prefix grammar before use. - The skill anchor's schema segment is now reduced to alphanumerics, matching SkillLayout.MintBundleSchemaName. A dotted --name previously smuggled an extra level into <prefix>.skill.<segment>. - Payload archives are copied verbatim again. Only a root-level <folder>.zip is skipped, because it collides with the bundle the extension mints, and that skip is now reported instead of silent. - Sidecar exclusion (metadata.*, README.md) is case-insensitive, so a Metadata.json no longer leaks into the imported skill. - readMetadata only swallows 404s; a rate limit or outage now fails the list instead of quietly returning a short gallery. - download clears a previous copy of the slug first, so files deleted upstream or left by a half-finished run do not linger, guarded by a containment check on the destination. - The pretty list no longer truncates slugs, and --all drops the inaccurate "for Copilot Studio" qualifier from its heading. - Test fixtures clean up their mkdtemp directories. reference/skill-schema.md and commands/add-skill.md are updated to match, including the .zip upload flow, which now asks the user to extract the archive rather than describing a step the command's allowed-tools cannot perform. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f42cd36d-11db-48e7-a770-959e3d0e40d7 (cherry picked from commit b515574)
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate findings remain in download handling, URL encoding, YAML/schema parsing, and schema-safe naming.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds an end-to-end workflow for acquiring Agent Skills locally or from the Cat Agent Skills gallery and importing them into Copilot Studio workspaces.
Changes:
- Adds gallery listing, downloading, and workspace import.
- Generates schema-safe
.mcs.ymlcompanions and handles sidecars and naming. - Adds documentation, shared schema guidance, and regression tests.
File summaries
| File | Summary |
|---|---|
scripts/test/add-skill.test.js |
Adds regression and behavior coverage. |
scripts/add-skill.js |
Implements gallery acquisition and workspace import. |
reference/skill-schema.md |
Defines authoritative skill layouts and naming rules. |
commands/add-skill.md |
Documents the /add-skill workflow. |
agents/copilot-studio-architect.md |
References shared skill-schema guidance. |
Review details
Suppressed comments (6)
reference/skill-schema.md:50
- This inline example contradicts the rule below that inline
componentNameanddescriptionmust always be double-quoted. Because this file is the authoritative schema, readers may copy the unquoted example; quote both metadata fields here.
componentName: make-restaurant-reservation
description: "Guides the user through making a restaurant reservation."
scripts/add-skill.js:525
fileSchemaSegment()can return an empty string for a valid payload filename containing no ASCII alphanumerics (for example#or...). The generated name then has the shape<prefix>.file._<token>, which has no component segment and can be rejected even though the importer otherwise promises valid schema names. Apply the same non-empty fallback used for the anchor before fitting the segment.
const fileSeg = fitSchemaSegment(fileSchemaSegment(leaf), {
scripts/add-skill.js:190
- Submission slugs come from repository directory names and are interpolated raw here. A valid slug containing
#or?is parsed as a URL fragment/query, so metadata lookup becomes a 404 or wrong request andlistcan silently omit that skill. Encode the slug (and use the same path-segment encoding for the other gallery URLs).
text = await fetchText(`${RAW_BASE}/submissions/${slug}/metadata.json`);
scripts/add-skill.js:280
- The prebuilt bundle URL uses the raw slug as well. A gallery slug containing URL-reserved characters will make the optional bundle fetch fail even though the tree lookup found the skill; encode the slug before constructing this URL.
const buf = await fetchBuffer(`${PAGES_BASE}/bundles/${slug}.zip`);
scripts/add-skill.js:325
- Windows reserves device names even when an extension follows, for example
CON.txt. The exact-string check lets--name CON.txtthrough, so the subsequent workspace path is unusable on Windows despite the documented sanitization. Check the stem before the first dot when applyingRESERVED_DEVICE_NAMES.
if (RESERVED_DEVICE_NAMES.has(s.toLowerCase())) s = `skill-${s}`;
scripts/add-skill.js:431
- This introduces a second settings parser that scrapes text with a regex, while the existing workspace consumer loads
settings.mcs.ymlwith js-yaml and reads the rootschemaName(scripts/src/chat-with-agent.js:346-363). Valid YAML scalar forms such as block/folded values or standard quoted escapes are rejected here and silently fall back to a bare skill, so the importer can disagree with the rest of the workspace tooling; parse the document (or explicitly validate and report the narrower accepted syntax) before falling back.
const m = raw.replace(/^\uFEFF/, '').match(/^schemaName:[ \t]*(.*)$/m);
- Files reviewed: 5/5 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Encode every gallery URL path segment, surface malformed metadata and non-404 bundle failures, and clear stale sibling bundle archives before refresh. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f42cd36d-11db-48e7-a770-959e3d0e40d7
Quote YAML timestamps, disambiguate Windows device-name stems, provide a non-empty file schema fallback, and make the documented narrow settings parser fallback explicit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f42cd36d-11db-48e7-a770-959e3d0e40d7
Bot review follow-upThe five inline findings are fixed, replied to, and resolved. The review also reported six suppressed findings without resolvable threads:
Additional regression coverage was written failing first for every behavioral change. The full suite now passes 43/43, including 34 focused add-skill tests. All three YAML examples in |
Stage payloads and optional bundles under the destination, then replace the previous download only after all required requests succeed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f42cd36d-11db-48e7-a770-959e3d0e40d7
There was a problem hiding this comment.
🔵 Needs a closer look
The importer mishandles common folded descriptions and does not quote YAML 1.1 binary integer scalars.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
scripts/add-skill.js:482
- Gallery manifests commonly use folded YAML descriptions such as
description: >-followed by indented text (for example,submissions/accessibility-pass/SKILL.md). This regex captures only the>-header, soplainYamlValuereturns>-and the generated anchor recordsdescription: ">-"instead of the skill's actual description, contrary to the schema's requirement to readdescriptionfrom the frontmatter. Parse the frontmatter YAML or explicitly support folded/block scalars before emitting the anchor description.
scripts/add-skill.js:491 - The YAML 1.1 number regex omits binary integers such as
0b101.yamlScalar("0b101")therefore emits a plain scalar that a YAML 1.1 consumer reads as numeric 5 rather than the required string, contradicting the scalar-safety rules inreference/skill-schema.md; add the binary form and a regression test.
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Gallery metadata.yaml submissions are omitted, and single-file imports can be rejected when only the file-component budget is exhausted.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
scripts/add-skill.js:206
- This only fetches
metadata.json, so submissions using the gallery's documentedmetadata.yamlsidecar are treated as having no metadata:listSkillsfilters them out, anddownloadSkillloses the catalog display name. Support the accepted metadata formats (or report an explicit unsupported-format error) instead of interpreting a valid YAML sidecar as a missing skill.
scripts/add-skill.js:631 - This rejects a valid single-file skill when the agent prefix leaves room for the anchor but not for a file component. A single
SKILL.mdemits only the anchor (the reference says it has nomanifestSchemaName, bundle, or sidecars), butassertSchemaBudgetalways checks both shapes beforepayloadRelis known. Only require the file-shape budget when the source contains a non-manifest payload; otherwise a prefix of length 88, for example, is unnecessarily rejected even though the anchor can exactly fit 100 characters.
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Lite
Reject workspaces whose root settings.mcs.yml does not declare a versioned cliagent template before creating or replacing any skill files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f42cd36d-11db-48e7-a770-959e3d0e40d7
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate findings remain in scripts/add-skill.js.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (7)
Previously missed (2) — in code that hasn't changed since the last review.
scripts/add-skill.js:217
JSON.parsecan succeed with a non-object such asnullor an array, but those values are not metadata with the fields consumed below.listSkillssilently filtersnull(or accepts another invalid shape with defaults), returning a success-shaped partial/incorrect listing despite the documented malformed-metadata handling; validate that the parsed value is a non-null object and reject arrays before returning it.
scripts/add-skill.js:529YAML11_NUMBERomits binary integer literals such as0b101.yamlScalar("0b101")therefore emits a plain scalar that a YAML 1.1 consumer reads as an integer, violating the documented string-valuedcomponentName/descriptioncontract; include the binary form and add a regression test.
scripts/add-skill.js:520
- Gallery manifests commonly use a YAML block scalar for
description(for example,description: >-followed by indented text). This regex captures only the>-marker, so the generated anchor writesdescription: ">-"and loses the actual selection description. Parse the frontmatter YAML or explicitly unfold|/>scalars before populatingmcs.metadata.description.
const m = fm[1].match(/^description:[ \t]*(.*)$/m);
if (!m) return '';
return plainYamlValue(m[1]);
scripts/add-skill.js:212
- The gallery contract allows a submission to use
metadata.yamlormetadata.yml, and this importer already treats those names as gallery sidecars, butreadMetadatarequests onlymetadata.json. Such a valid skill gets a 404/null result and is filtered out oflistSkills, so it never appears in the gallery. Try the supported metadata filenames and parse the YAML forms before treating metadata as absent.
text = await fetchText(submissionUrl(slug, 'metadata.json'));
} catch (e) {
// A missing metadata.json simply means "not a listable skill". Any other
// failure (rate limit, outage) must surface instead of silently shrinking
// the gallery listing to whatever happened to succeed.
scripts/add-skill.js:782
- The user-facing pretty-list heading renders
1 skills, and the new test codifies that wording. Use the singularskillwhen the count is one and update the corresponding expectation.
const heading = `Cat Agent Skills ${skills.length} skills${all ? '' : ' for Copilot Studio'}`;
scripts/add-skill.js:191
files.some(isSkillFile)also matches nested paths such asdocs/SKILL.md. That can classify a non-skill submission as an unpacked skill even though the downloaded folder has no top-level manifest and will later be rejected byimportSkill; only recognizeSKILL.md/skill.mdat the submission root.
const hasSkill = files.some(isSkillFile);
scripts/add-skill.js:186
- This excludes any path whose first segment is named like a gallery sidecar, not just a sidecar file at the submission root. A valid payload such as
metadata.json/schema.jsonis therefore omitted from downloads andhasBundledetection, despite the documented root-only exclusion; test the full relative path for a root-level sidecar instead.
function payloadFiles(files) {
return files.filter((f) => !isSidecarLeaf(topName(f)));
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Lite
…calars Addresses two open review comments on the add-skill script: - importSkill now stages the skill in a sibling temp folder and only removes/replaces behaviors/<name>/ via an atomic rename after every copy and companion write succeeds. A failed --force re-import no longer deletes or half-overwrites the previous skill. - plainYamlValue now requires a closing quote to be followed only by whitespace or a # comment, so malformed settings such as template: "cliagent-1.0" unexpected or schemaName: "crbab_x" unexpected no longer pass validation, restoring the malformed-workspace/no-write guarantee. Adds regression tests for both. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e528fdea-3aa5-44b9-b9db-0a30583bf112
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate issues remain in atomic promotion, metadata validation, slug rendering, and YAML scalar handling.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (6)
Previously missed (5) — in code that hasn't changed since the last review.
scripts/add-skill.js:221
- Only JSON syntax errors are rejected here. A syntactically valid but malformed metadata document such as
nullis returned asnulland then silently filtered bylistSkills, while invalid types such as a numericnamecan later fail duringtoLowerCase()without naming the gallery entry. Validate the parsed metadata shape (at least a non-null object and the fields consumed as strings/arrays) and throw the annotated metadata error instead of producing a partial or crashing listing.
scripts/add-skill.js:323 - The refresh is only request-atomic, not promotion-atomic: after these removals, either
renameSynccan still fail (for example because of a local filesystem/locking error), leaving the function rejected with the previous skill and bundle already deleted. This contradicts the documented guarantee that a failed refresh preserves a previously good download; retain a rollback/backup or use an atomic replacement strategy for the local promotion.
scripts/add-skill.js:452 - The quoted-scalar parser only decodes
\"and\\; valid YAML escapes such as\u0064remain literal. ConsequentlyschemaName: "crbab_\u0064emo"is rejected and silently falls back to a bare skill even though the documented parser contract says quoted values are unescaped. Use a full YAML scalar decoder or implement all supported double-quoted escapes before validation.
scripts/add-skill.js:537 - YAML 1.1 also resolves binary integer scalars such as
0b101, but this pattern does not match them, soyamlScalar("0b101")emits an unquoted componentName and a YAML consumer reads it as number 5. Add the binary form (and a regression test) so generated metadata remains string-valued.
scripts/add-skill.js:766 fitCellnormalizes whitespace for every column, includingSlug, so a valid slug containing repeated or leading/trailing spaces is displayed differently from the exact key accepted bydownload --slug. Keep slug rendering lossless (while retaining whitespace normalization for names and descriptions), otherwise the table does not always preserve the download slug as promised.
scripts/add-skill.js:810
list --prettyrenders1 skillsfor a single result, which is grammatically incorrect user-facing output. Pluralize the heading conditionally so one result is reported as1 skill.
const heading = `Cat Agent Skills ${skills.length} skills${all ? '' : ' for Copilot Studio'}`;
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
Summary
Adds an end-to-end
/add-skillworkflow that can acquire an Agent Skill from either a local upload or the Power CAT Cat Agent Skills gallery, then materialize it in a cloned Copilot Studio CLI-agent workspace.list --prettytable that preserves the exact download slug.SKILL.mdand its payload underbehaviors/<name>/, with--name,--force,--include-sidecars, and--no-sidecarscontrols.skill.mcs.ymlas the anchor and<payload>.mcs.ymlbeside each non-manifest payload file. The anchor declares the implicitSKILL.mdcomponent throughmanifestSchemaName.reference/skill-schema.md; the command and architect agent now point to that source of truth.Safety and correctness
The importer now:
template: cliagent-<version>value insettings.mcs.ymland rejects unsupported workspaces before creating or replacing any skill files, even with--force.schemaNamevalues fromsettings.mcs.ymland validates the Dataverse prefix before using it.<folder>.zipcollision is skipped, with an explicit warning..mcs.ymlcompanions, and gives non-alphanumeric payload filenames a stablefileschema segment fallback.CON.txt.Command documentation
commands/add-skill.mddocuments the acquire-then-import flow, CLI-agent-only cloned-workspace requirement, temporary downloads, warning/error handling, and the local.ziplimitation: because the command's allowed tools do not include an archive extractor, the user must provide the extracted folder.Verification
npm test: 47/47 passingreference/skill-schema.mdparse successfully..zip, mixed-case gallery sidecar, and nested payload sidecars.