Skip to content

Add Agent Skill gallery acquisition and workspace import - #35

Merged
Anderson Silva (anderson-joyle) merged 9 commits into
mainfrom
andersonf/add-agent-skill-2
Sep 17, 2026
Merged

Anderson Silva (anderson-joyle) merged 9 commits into
mainfrom
andersonf/add-agent-skill-2

Conversation

@anderson-joyle

@anderson-joyle Anderson Silva (anderson-joyle) commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an end-to-end /add-skill workflow 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.

  • Browse gallery skills through JSON or a deterministic list --pretty table that preserves the exact download slug.
  • Download the unpacked skill into an explicit destination or a temporary default directory, with the gallery bundle retained when available.
  • Import SKILL.md and its payload under behaviors/<name>/, with --name, --force, --include-sidecars, and --no-sidecars controls.
  • Emit the current Copilot Studio nested skill layout: skill.mcs.yml as the anchor and <payload>.mcs.yml beside each non-manifest payload file. The anchor declares the implicit SKILL.md component through manifestSchemaName.
  • Centralize both supported skill shapes, naming rules, YAML encoding, and schema-name constraints in reference/skill-schema.md; the command and architect agent now point to that source of truth.

Safety and correctness

The importer now:

  • Requires a root template: cliagent-<version> value in settings.mcs.yml and rejects unsupported workspaces before creating or replacing any skill files, even with --force.
  • Parses quoted and inline-commented schemaName values from settings.mcs.yml and validates the Dataverse prefix before using it.
  • Keeps generated component schema names within Dataverse's 100-character limit, warning when a segment must be truncated and failing before writing when no valid segment can fit.
  • Removes non-alphanumerics from the anchor schema segment so dotted folder names cannot introduce extra schema levels.
  • Copies payload archives verbatim; only a root-level <folder>.zip collision is skipped, with an explicit warning.
  • Excludes gallery sidecars case-insensitively, regenerates all .mcs.yml companions, and gives non-alphanumeric payload filenames a stable file schema segment fallback.
  • URL-encodes gallery slugs and every payload path segment before requesting metadata, payload files, or bundles.
  • Stages each refresh under the destination and replaces the prior unpacked folder and sibling bundle only after all required requests succeed, so stale files are removed without sacrificing a previously good download on failure.
  • Surfaces malformed gallery metadata and failures other than a genuine 404 instead of returning a success-shaped partial list.
  • Treats only a missing optional bundle (404) as non-fatal; network and server failures remain retryable errors.
  • Quotes YAML 1.1 ambiguous scalars, including timestamps, in generated metadata.
  • Disambiguates Windows reserved device-name stems even when an extension follows, such as CON.txt.

Command documentation

commands/add-skill.md documents the acquire-then-import flow, CLI-agent-only cloned-workspace requirement, temporary downloads, warning/error handling, and the local .zip limitation: because the command's allowed tools do not include an archive extractor, the user must provide the extracted folder.

Verification

  • npm test: 47/47 passing
  • 38 focused add-skill tests cover the behavior and review findings, including CLI-agent workspace rejection, no-write failure behavior, quoted/commented settings values, dotted names, nested archives, sidecar casing, stale downloads, metadata errors, slug rendering, headings, and temporary-directory cleanup.
  • All YAML examples in reference/skill-schema.md parse successfully.
  • End-to-end import smoke-tested with a quoted and commented schema prefix, dotted skill name, nested .zip, mixed-case gallery sidecar, and nested payload sidecars.

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)
Copilot AI lite review requested due to automatic review settings September 17, 2026 20:14
@anderson-joyle
Anderson Silva (anderson-joyle) marked this pull request as ready for review September 17, 2026 20:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.yml companions 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 componentName and description must 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 and list can 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.txt through, so the subsequent workspace path is unusable on Windows despite the documented sanitization. Check the stem before the first dot when applying RESERVED_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.yml with js-yaml and reads the root schemaName (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.

Comment thread scripts/add-skill.js
Comment thread scripts/add-skill.js Outdated
Comment thread scripts/add-skill.js Outdated
Comment thread scripts/add-skill.js Outdated
Comment thread scripts/add-skill.js Outdated
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
@anderson-joyle

Anderson Silva (anderson-joyle) commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Bot review follow-up

The five inline findings are fixed, replied to, and resolved. The review also reported six suppressed findings without resolvable threads:

Suppressed finding Resolution
Inline example contradicted the always-quoted rule Fixed in 21e6867: componentName is now double-quoted.
A non-alphanumeric payload name produced an empty file segment Fixed in 21e6867: fileSchemaSegment() falls back to file, matching the extension generator's non-empty behavior.
Metadata URL used a raw slug Fixed in 03b07e6 through submissionUrl().
Bundle URL used a raw slug Fixed in 03b07e6 through bundleUrl().
CON.txt bypassed Windows reserved-name protection Fixed in 21e6867: the stem before the first dot is checked. This matters even though Node can create the path, because Git for Windows cannot index it.
Settings parser did not accept every valid YAML scalar form Intentionally kept dependency-free. The fallback warning now states that a root, single-line plain or quoted schemaName: is required, and reference/skill-schema.md documents that contract. Block scalar support was not added.

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 reference/skill-schema.md also parse successfully.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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, so plainYamlValue returns >- and the generated anchor records description: ">-" instead of the skill's actual description, contrary to the schema's requirement to read description from 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 in reference/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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 documented metadata.yaml sidecar are treated as having no metadata: listSkills filters them out, and downloadSkill loses 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.md emits only the anchor (the reference says it has no manifestSchemaName, bundle, or sidecars), but assertSchemaBudget always checks both shapes before payloadRel is 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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.parse can succeed with a non-object such as null or an array, but those values are not metadata with the fields consumed below. listSkills silently filters null (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:529
  • YAML11_NUMBER omits binary integer literals such as 0b101. yamlScalar("0b101") therefore emits a plain scalar that a YAML 1.1 consumer reads as an integer, violating the documented string-valued componentName/description contract; 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 writes description: ">-" and loses the actual selection description. Parse the frontmatter YAML or explicitly unfold |/> scalars before populating mcs.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.yaml or metadata.yml, and this importer already treats those names as gallery sidecars, but readMetadata requests only metadata.json. Such a valid skill gets a 404/null result and is filtered out of listSkills, 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 singular skill when 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 as docs/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 by importSkill; only recognize SKILL.md/skill.md at 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.json is therefore omitted from downloads and hasBundle detection, 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

Comment thread scripts/add-skill.js Outdated
Comment thread scripts/add-skill.js
…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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 null is returned as null and then silently filtered by listSkills, while invalid types such as a numeric name can later fail during toLowerCase() 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 renameSync can 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 \u0064 remain literal. Consequently schemaName: "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, so yamlScalar("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
  • fitCell normalizes whitespace for every column, including Slug, so a valid slug containing repeated or leading/trailing spaces is displayed differently from the exact key accepted by download --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 --pretty renders 1 skills for a single result, which is grammatically incorrect user-facing output. Pluralize the heading conditionally so one result is reported as 1 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

Comment thread scripts/add-skill.js
@anderson-joyle
Anderson Silva (anderson-joyle) merged commit a0833db into main Sep 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants