Skip to content

Add skill import into cloned agent workspaces, plus pretty gallery listing - #32

Closed
Anderson Silva (anderson-joyle) wants to merge 4 commits into
microsoft:mainfrom
anderson-joyle:andersonf/add-agent-skill
Closed

Anderson Silva (anderson-joyle) wants to merge 4 commits into
microsoft:mainfrom
anderson-joyle:andersonf/add-agent-skill

Conversation

@anderson-joyle

Copy link
Copy Markdown
Contributor

Extends the /add-skill command beyond acquiring a skill so it can also materialize it into a cloned Copilot Studio agent workspace, and makes 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.
  • Deterministic sort order (code-point, slug tiebreaker).

commands/add-skill.md

  • Documents the new acquire-then-import flow, the standalone source question, the --pretty table, temp-folder downloads, and the import step. Clarifies that publishing to the cloud happens from the VS Code Copilot Studio extension — this command never pushes.

@anderson-joyle
Anderson Silva (anderson-joyle) marked this pull request as ready for review September 14, 2026 17:00
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
…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>

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 import, sidecar, schema naming, download, and documentation issues block approval.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This pull request adds skill import into cloned Copilot Studio workspaces and improves gallery browsing.

Changes:

  • Adds workspace import with optional .mcs.yml sidecars.
  • Adds deterministic list --pretty output and temporary download defaults.
  • Documents the updated acquisition, import, and publishing workflow.
File summaries
File Final findings
commands/add-skill.md Moderate (1 vote): Documents local .zip uploads, but the permitted command has no extraction path and import accepts only directories.
scripts/add-skill.js Critical (1 vote): Generated sidecars do not match the required root-level Copilot Studio layout and omit the SKILL.md companion.
Moderate (3 votes): Drops .zip payload files, violating verbatim copying.
Moderate (2 votes): Truncates long slugs in pretty output, making downloads unusable.
Moderate (1 vote): Hides non-404 metadata failures and can return incomplete galleries.
Moderate (1 vote): CRLF schemaName files can silently lose schema prefixes.
Moderate (1 vote): Does not enforce the 100-character schema-name limit.
Moderate (1 vote): Does not safely escape relative paths in generated YAML.
Moderate (1 vote): Re-downloads can retain stale or partial files.
Moderate (1 vote): Dots in --name create invalid extra schema segments.
Nit (1 vote): --no-sidecars omits the documented fallback warning.
Review details

Suppressed comments (8)

commands/add-skill.md:64

  • This workflow advertises local .zip upload support, but the only allowed shell command is node ...add-skill.js, and that script has no extraction command; import accepts only a directory containing SKILL.md. Consequently a user who supplies a zip cannot reach the import step as documented. Add an extraction path/subcommand and permit it, or remove .zip from the accepted local-upload flow.
3. Confirm the resolved absolute path. For a `SKILL.md`, its containing folder is the `--src` for
   import (step 5). For a `.zip`, extract it to a folder first so `--src` points at the extracted
   `SKILL.md` and any payload files.

scripts/add-skill.js:324

  • This parser only matches LF-terminated, unquoted schemaName lines. A valid Windows checkout can leave settings.mcs.yml with CRLF, so the trailing \r prevents a match and every import silently falls back to a bare skill without the required companions. Allow the line ending (and ideally parse the YAML scalar) before deciding the prefix is unavailable.
    const m = raw.match(/^\uFEFF?schemaName:[ \t]*(\S+)[ \t]*$/m);

scripts/add-skill.js:175

  • readMetadata treats every failure as "metadata is absent". If the raw GitHub request is rate-limited or the network is unavailable after the tree request succeeds, list silently filters out those skills and returns a successful, incomplete gallery instead of the documented transient error. Only suppress a confirmed 404; rethrow other failures.
  } catch {
    return null;
  }

scripts/add-skill.js:391

  • The generated schema names are not budgeted against the Dataverse 100-character limit. sanitizeFolderName caps only the folder segment, so a long workspace schemaName plus .skill., the folder, and the token can exceed 100 and make sync/import fail; the same issue exists for .file. sidecars. The repository's schema guidance requires calculating the remaining slug budget before writing derived names (see reference/knowledge-schema.md:159-171).
  anchorLines.push(`  schemaName: ${prefix}.skill.${folder}_${schemaToken(3)}`);
  if (isBundle) {
    anchorLines.push(`  bundle: ${prefix}.file.${fileSchemaSegment(`${folder}.zip`)}_${schemaToken(5)}`);
    anchorLines.push(`  manifestSchemaName: ${prefix}.file.${fileSchemaSegment(MANIFEST_NAME)}_${schemaToken(5)}`);

scripts/add-skill.js:492

  • When --no-sidecars is supplied, this branch skips both companion generation and the warning, so the result does not explain why it fell back to a bare skill. This contradicts the documented behavior for this flag; add an explicit warning in the noSidecars branch.
  if (!noSidecars) {
    schemaPrefix = readAgentSchemaPrefix(wsDir);

scripts/add-skill.js:404

  • rel comes from arbitrary local filenames, but it is interpolated as a plain YAML scalar. A payload such as foo #bar.txt or foo: bar.txt can therefore be truncated or make the sidecar invalid; use the existing yamlScalar helper for this path value so every generated companion remains valid YAML.
        `  componentName: ./${rel}`,

scripts/add-skill.js:227

  • Re-downloading a slug into an existing destination leaves files from the previous or partial download in place. If a gallery file was removed (or a retry follows a partial failure), those stale files are then returned in dir and copied by import, so the workspace no longer reflects the selected skill. Build into a fresh staging directory or remove/reconcile the old skill directory before writing.
  const skillDir = path.join(dest, slug);
  fs.mkdirSync(skillDir, { recursive: true });

scripts/add-skill.js:286

  • . is allowed here even though folder is interpolated into prefix.skill.${folder}_...; --name foo.bar therefore generates prefix.skill.foo.bar_..., treating the folder as an extra schema segment instead of the single <segment> promised above. Replace . with - (or derive a separate schema-safe segment) before writing companions.
    .replace(/[^A-Za-z0-9._-]+/g, '-')
  • Files reviewed: 2/2 changed files
  • Comments generated: 3
  • 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
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

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.

Note

Copilot was unable to run its full agentic suite in this review.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

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

Copy link
Copy Markdown
Contributor Author

Review follow-up - b515574

All actionable bot findings are addressed. Each inline thread has a reply and is resolved, except the one rated Critical, which I believe is incorrect - see that thread for the disproof against the extension's SkillLayout/SkillBodyProjection source.

The review also listed several findings as suppressed rather than as inline threads, so they have no thread to resolve. Status of those:

Finding Status
Schema names could exceed the 100-char Dataverse cap Already fixed in 56c0cef
componentName: ./<rel> was not YAML-escaped Already fixed in 56c0cef
A . in --name adds an extra schema-name segment Fixed here - reproduced crbab_demo_dcF_b3.skill.foo.bar_GiB; the anchor segment is now reduced to alphanumerics, matching SkillLayout.MintBundleSchemaName
readMetadata swallowed non-404 errors Fixed here - only 404 returns null; a rate limit or outage now fails the listing instead of silently returning a short gallery
Re-downloading a slug left stale/partial files behind Fixed here - the previous copy is cleared first, guarded by a containment check on the destination
--no-sidecars did not emit the documented fallback warning Docs corrected - no warning is emitted when the bare layout was explicitly requested
commands/add-skill.md documented a .zip flow that allowed-tools cannot perform Docs corrected - the command now asks the user to extract the archive, since Bash(node *add-skill.js*) grants no extraction tool
CRLF line endings break the schemaName regex Not reproducible - ECMAScript multiline $ matches before \r as well as \n. Verified against a CRLF settings.mcs.yml: the prefix parsed and all companions were written

Verification: 33/33 tests pass (15 new, each written failing first), all three YAML examples in reference/skill-schema.md parse, and an end-to-end import with a quoted+commented prefix, a dotted --name, a nested assets/templates.zip and a Metadata.json produces the expected anchor with no warnings.

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

Eleven unresolved moderate findings affect YAML parsing, downloads, portability, and generated schema metadata.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (10)

Previously missed (3) — in code that hasn't changed since the last review.

scripts/add-skill.js:325

  • Windows reserves CON, COM1, and the other device names even when an extension is appended, so a name such as con.txt passes this check and then cannot be created as a folder. Check the basename before the first dot rather than only the full sanitized string.
    scripts/add-skill.js:451
  • Gallery bundle manifests use folded block scalars such as description: >- (for example, submissions/acroform-writer/SKILL.md). This regex captures only the >- indicator, so the generated anchor gets description: ">-" instead of the actual trigger text and loses the metadata used to select the skill. Parse the frontmatter as YAML or handle folded scalars before writing the anchor.
    scripts/add-skill.js:460
  • YAML11_NUMBER omits binary integer literals such as 0b101. A folder or description with that value is emitted unquoted, but a YAML 1.1 reader resolves it to the number 5, violating the documented string-scalar rules and corrupting componentName/description. Add the binary form to this regex or use the YAML parser.

scripts/add-skill.js:202

  • A 200 response containing malformed metadata.json is treated as if the file were absent, and listSkills then filters the skill out via r.meta. This still silently shrinks the gallery despite the preceding logic explicitly surfacing non-404 failures; propagate a parse error (including the slug) instead.
  try {
    return JSON.parse(text);
  } catch {
    return null;
  }

scripts/add-skill.js:401

  • The double-quoted branch decodes only \" and \\; valid YAML escapes such as \u0063 remain literal, fail SCHEMA_PREFIX_RE, and force a bare import instead of emitting companions. The existing settings loader in scripts/src/chat-with-agent.js:346 uses js-yaml for this file; use the same parser or implement complete YAML escape decoding here.
    const m = v.match(/^"((?:[^"\\]|\\.)*)"/);
    return m ? m[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\') : '';

scripts/add-skill.js:168

  • payloadFiles applies the sidecar-name check to topName(f) for every path, so a legitimate payload below a root directory named metadata.json (for example metadata.json/data.txt) is dropped from both bundle classification and download. The schema docs specify that these names are excluded only at the skill root; gate this check on the path having no /.
  return files.filter((f) => !isSidecarLeaf(topName(f)));

scripts/add-skill.js:269

  • rel is interpolated into the raw GitHub URL without escaping path components. A valid payload such as scripts/run #1.py is parsed by fetch with #1.py as a fragment (and ? would become a query), so the download requests the wrong file even though the importer can handle that path. Encode each URL path segment before fetching.
    const buf = await fetchBuffer(`${RAW_BASE}/submissions/${slug}/${rel}`);

scripts/add-skill.js:351

  • A payload leaf containing no ASCII alphanumeric characters (for example ... or an emoji-only filename) becomes an empty segment, producing schema names such as <prefix>.file._abcde. That does not satisfy the documented <segment>_<token> shape and will be rejected instead of importing the otherwise valid payload; use a non-empty fallback or reject this case before writing.
function fileSchemaSegment(fileName) {
  return String(fileName || '').toLowerCase().replace(/[^a-z0-9]+/g, '');

scripts/add-skill.js:480

  • The YAML-1.1 safety check omits implicit timestamps. Values such as 2026-09-17 can therefore be emitted as plain componentName/description scalars and deserialize as dates rather than strings, despite the reference promising safe strings. Add the YAML-1.1 timestamp forms to the quote decision and cover them with a test.
    YAML11_BOOL.test(s) ||
    YAML11_NULL.test(s) ||
    YAML11_NUMBER.test(s) ||
    YAML11_SEXAGESIMAL.test(s);

scripts/add-skill.js:286

  • Every error from the optional bundle fetch is discarded. If hasBundle is true but the bundle endpoint returns a rate-limit/server/network error, the command still reports success with zip: null and no warning, hiding an incomplete acquisition; at least rethrow non-404 failures or return them as warnings in the result.
    } catch (e) {
      // Non-fatal: the unpacked payload above is the source of truth.
      zipPath = null;
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread scripts/add-skill.js
Comment on lines +261 to +262
fs.rmSync(skillDir, { recursive: true, force: true });
fs.mkdirSync(skillDir, { recursive: true });
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.

2 participants