Add skill import into cloned agent workspaces, plus pretty gallery listing - #32
Anderson Silva (anderson-joyle) wants to merge 4 commits into
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
…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>
0d52408 to
962abe8
Compare
There was a problem hiding this comment.
🟡 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.ymlsidecars. - Adds deterministic
list --prettyoutput 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
.zipupload support, but the only allowed shell command isnode ...add-skill.js, and that script has no extraction command;importaccepts only a directory containingSKILL.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.zipfrom 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
schemaNamelines. A valid Windows checkout can leavesettings.mcs.ymlwith CRLF, so the trailing\rprevents 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
readMetadatatreats every failure as "metadata is absent". If the raw GitHub request is rate-limited or the network is unavailable after the tree request succeeds,listsilently 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.
sanitizeFolderNamecaps only the folder segment, so a long workspaceschemaNameplus.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 (seereference/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-sidecarsis 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 thenoSidecarsbranch.
if (!noSidecars) {
schemaPrefix = readAgentSchemaPrefix(wsDir);
scripts/add-skill.js:404
relcomes from arbitrary local filenames, but it is interpolated as a plain YAML scalar. A payload such asfoo #bar.txtorfoo: bar.txtcan therefore be truncated or make the sidecar invalid; use the existingyamlScalarhelper 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
dirand copied byimport, 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 thoughfolderis interpolated intoprefix.skill.${folder}_...;--name foo.bartherefore generatesprefix.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.
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
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
Review follow-up - b515574All 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 The review also listed several findings as suppressed rather than as inline threads, so they have no thread to resolve. Status of those:
Verification: 33/33 tests pass (15 new, each written failing first), all three YAML examples in |
There was a problem hiding this comment.
🟡 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 ascon.txtpasses 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 getsdescription: ">-"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_NUMBERomits binary integer literals such as0b101. A folder or description with that value is emitted unquoted, but a YAML 1.1 reader resolves it to the number5, violating the documented string-scalar rules and corruptingcomponentName/description. Add the binary form to this regex or use the YAML parser.
scripts/add-skill.js:202
- A 200 response containing malformed
metadata.jsonis treated as if the file were absent, andlistSkillsthen filters the skill out viar.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\u0063remain literal, failSCHEMA_PREFIX_RE, and force a bare import instead of emitting companions. The existing settings loader inscripts/src/chat-with-agent.js:346usesjs-yamlfor 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
payloadFilesapplies the sidecar-name check totopName(f)for every path, so a legitimate payload below a root directory namedmetadata.json(for examplemetadata.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
relis interpolated into the raw GitHub URL without escaping path components. A valid payload such asscripts/run #1.pyis parsed byfetchwith#1.pyas 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-17can therefore be emitted as plaincomponentName/descriptionscalars 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
hasBundleis true but the bundle endpoint returns a rate-limit/server/network error, the command still reports success withzip: nulland 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
| fs.rmSync(skillDir, { recursive: true, force: true }); | ||
| fs.mkdirSync(skillDir, { recursive: true }); |
Extends the
/add-skillcommand 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
import --src <dir> --workspace <agentDir>command that writes the skill underbehaviors/<name>/:SKILL.mdplus every payload file copied verbatim. By default it also emits portal-style.mcs.ymlcompanions (an anchorskill.mcs.ymlcarrying theInlineAgentSkillidentity, plus one<file>.mcs.ymlsidecar per non-manifest file for bundle skills) so the on-disk layout matches a Copilot Studio portal import. Schema names are prefixed with the agentschemaNameread fromsettings.mcs.yml; when the prefix is unavailable or--no-sidecarsis passed, it falls back to a barebehaviors/skill and warns. Supports--name,--force, and--include-sidecars, and surfaces non-fatal workspace checks as warnings.list --prettyrenders a deterministic, fixed-width box table (with a Slug column and bundle marker) instead of JSON.download --destis now optional and defaults to a temp folder (<os-tmp>/mcs-add-skill) so gallery downloads don't clutter the cwd.commands/add-skill.md
--prettytable, 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.