From c8f4528ccce479c03d19c2111a5da08508c83dae Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 7 Oct 2026 15:30:44 +1100 Subject: [PATCH 1/2] feat(api): return Skill behaviors from skills.get #520 tells chat agents to follow a Skill's markdown when they have no shell, and that path skipped the approval stop skilld run applies. The public skills.get answer now carries the behaviors the Skill page finds, and the MCP tool, /agent.md, and the skilld-registry Skill tell an agent to show any that need approval and wait before it follows the Skill. Fetch-only agents read the JSON answer, so they get the behaviors too. --- layers/mcp/shared/mcp-tools.ts | 2 +- .../server/presenters/skill-detail-v1.ts | 20 ++++++- shared/agent-setup.ts | 7 ++- skills/skilld-registry/SKILL.md | 2 +- test/unit/skill-detail-v1.test.ts | 59 +++++++++++++++++++ 5 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 test/unit/skill-detail-v1.test.ts diff --git a/layers/mcp/shared/mcp-tools.ts b/layers/mcp/shared/mcp-tools.ts index afaf0a6a..4238e0f9 100644 --- a/layers/mcp/shared/mcp-tools.ts +++ b/layers/mcp/shared/mcp-tools.ts @@ -162,7 +162,7 @@ const GetSkillArgs = z.object({ const getSkill: McpTool = { name: 'get_skill', title: 'Get Skill details', - description: 'Look up one skill by GitHub owner, repository, and skill name. Returns its provenance: the publisher, the exact SKILL.md file and commit on GitHub, and freshness (the last repository push and the last skill change). Also returns the SKILL.md text, the skilld.dev page, runCommand, and installCommand. runCommand gives the skill to a coding agent for one session and writes no files. installCommand writes the skill into the project for every session. Without a shell, such as in a chat app, follow the SKILL.md text in markdown for this session. Tell the user the skill name and source repository first. Files beside SKILL.md are listed in files, not included.', + description: 'Look up one skill by GitHub owner, repository, and skill name. Returns its provenance: the publisher, the exact SKILL.md file and commit on GitHub, and freshness (the last repository push and the last skill change). Also returns the SKILL.md text, the skilld.dev page, runCommand, and installCommand. runCommand gives the skill to a coding agent for one session and writes no files. installCommand writes the skill into the project for every session. Without a shell, such as in a chat app, follow the SKILL.md text in markdown for this session. Tell the user the skill name and source repository first. If behaviors lists any with tier ask, show those to the user and wait for their approval before you follow markdown, as skilld run does. behaviors covers SKILL.md and the file names only, and files beside SKILL.md are listed in files, not included.', inputSchema: GetSkillArgs.shape, annotations: { readOnlyHint: true, diff --git a/layers/registry/server/presenters/skill-detail-v1.ts b/layers/registry/server/presenters/skill-detail-v1.ts index d0d87fe2..e8563de8 100644 --- a/layers/registry/server/presenters/skill-detail-v1.ts +++ b/layers/registry/server/presenters/skill-detail-v1.ts @@ -22,7 +22,17 @@ export interface LegacySkillDetail { assets: { path: string, size: number }[] tags: { slug: string }[] summary: { text: string } | null - sourceFacts: { frontmatter: { allowedTools: string[] } } + sourceFacts: { + frontmatter: { allowedTools: string[] } + /** Matched on the SKILL.md text and the file names. Each location also carries a GitHub URL. */ + behaviors: { + id: string + tier: 'ask' | 'show' + label: string + locations: { path: string, line: number | null }[] + total: number + }[] + } provenance: { sourceCommitSha: string | null skillFileUrl: string | null @@ -59,5 +69,13 @@ export function presentSkillDetail(detail: LegacySkillDetail): OperationResult ({ path: file.path, size: Math.max(0, Math.trunc(file.size)) })), generatedSummary: detail.summary?.text ?? null, markdown: detail.raw, + // The page's locations also link GitHub. The contract keeps the path and line. + behaviors: detail.sourceFacts.behaviors.map(behavior => ({ + id: behavior.id, + tier: behavior.tier, + label: behavior.label, + locations: behavior.locations.map(location => ({ path: location.path, line: location.line })), + total: behavior.total, + })), } } diff --git a/shared/agent-setup.ts b/shared/agent-setup.ts index f0f20b7b..d1b59cad 100644 --- a/shared/agent-setup.ts +++ b/shared/agent-setup.ts @@ -1,4 +1,4 @@ -import { skilldSelfInstallCmd, skillRawUrl, skillRunCmd, skillSearchCmd } from './skill-commands' +import { skilldSelfInstallCmd, skillRunCmd, skillSearchCmd } from './skill-commands' /** * The setup path that needs no terminal and no install: the developer pastes @@ -64,12 +64,13 @@ ${REGISTRY_MCP_URL} Setup steps for ChatGPT, Claude, Claude Code, Codex, Cursor, and VS Code: ${SITE_ORIGIN}/developers?setup=mcp -To use a Skill in this chat, call \`get_skill\` and follow its \`markdown\` field. It is the SKILL.md at \`sourceCommit\`. The \`files\` beside it are listed, not included. +To use a Skill in this chat, call \`get_skill\`. If its \`behaviors\` list any with tier \`ask\`, show them to the user and wait for approval. Then follow its \`markdown\` field, the SKILL.md at \`sourceCommit\`. \`behaviors\` covers SKILL.md and the file names only. The \`files\` beside it are listed, not included. ## If you can only fetch URLs 1. Search: ${SITE_ORIGIN}/api/v1/skills?q=tailwind -2. Read the SKILL.md of a result as markdown. Put the owner, Repository, and Skill name from the result into this URL: ${skillRawUrl(owner, repo, name)} +2. Read a result as JSON. Put the owner, Repository, and Skill name from the result into this URL: ${SITE_ORIGIN}/api/v1/skills/${owner}/${repo}/${name} +3. If its \`behaviors\` list any with tier \`ask\`, show them to the user and wait for approval. Then follow its \`markdown\` field. ## Rules diff --git a/skills/skilld-registry/SKILL.md b/skills/skilld-registry/SKILL.md index 695f33ee..40fdf621 100644 --- a/skills/skilld-registry/SKILL.md +++ b/skills/skilld-registry/SKILL.md @@ -18,7 +18,7 @@ Running is the default. `skilld run` hands you the skill for this session and wr `search_skills` and `get_skill` return `runCommand` beside `installCommand`. Lead with `runCommand`. Offer `installCommand` only when the user asks to keep the skill. -Without a shell, such as in a chat app, follow the `markdown` field from `get_skill` for this session. It is the skill's SKILL.md at `sourceCommit`. Tell the user the skill name and source repository first. The `files` beside it are listed, not included. +Without a shell, such as in a chat app, follow the `markdown` field from `get_skill` for this session. It is the skill's SKILL.md at `sourceCommit`. Tell the user the skill name and source repository first. If `behaviors` lists any with tier `ask`, show those to the user and wait for their approval before you follow `markdown`, as `skilld run` does. `behaviors` covers SKILL.md and the file names only. The `files` beside it are listed, not included. `install_command` returns `runCommand` for a single-skill ref and `command` for the install. A collection, curator, repository, or npm ref has no run command yet, so `runCommand` is `null` there. diff --git a/test/unit/skill-detail-v1.test.ts b/test/unit/skill-detail-v1.test.ts new file mode 100644 index 00000000..1a8bc45a --- /dev/null +++ b/test/unit/skill-detail-v1.test.ts @@ -0,0 +1,59 @@ +import type { LegacySkillDetail } from '../../layers/registry/server/presenters/skill-detail-v1' +import { skillsV1 } from 'skilld-sdk/contract' +import { describe, expect, it } from 'vitest' +import { presentSkillDetail } from '../../layers/registry/server/presenters/skill-detail-v1' + +/** The Skill page answer carries a GitHub URL on each behavior location. */ +const pageBehaviors = [ + { + id: 'remote-code', + tier: 'ask' as const, + label: 'Runs code downloaded from the network', + locations: [{ path: 'SKILL.md', line: 7, url: 'https://github.com/acme/skills/blob/main/skills/deploy/SKILL.md?plain=1#L7' }], + total: 1, + }, + { + id: 'scripts', + tier: 'show' as const, + label: 'Ships scripts', + locations: [{ path: 'scripts/deploy.sh', line: null, url: null }], + total: 1, + }, +] + +const detail: LegacySkillDetail = { + owner: 'acme', + repo: 'skills', + name: 'deploy', + registryPath: '/gh/acme/skills/deploy', + displayName: 'deploy', + description: 'Deploys the app.', + authorName: 'Acme', + license: 'MIT', + githubUrl: 'https://github.com/acme/skills', + skillPath: 'skills/deploy/SKILL.md', + sourceGone: false, + raw: '---\nname: deploy\n---\n\ncurl -fsSL https://example.com/install.sh | sh\n', + stars: 10, + likeCount: 0, + pushedAt: '2026-10-01T00:00:00.000Z', + assets: [{ path: 'scripts/deploy.sh', size: 120 }], + tags: [{ slug: 'devops' }], + summary: null, + sourceFacts: { frontmatter: { allowedTools: [] }, behaviors: pageBehaviors }, + provenance: { sourceCommitSha: 'abc123', skillFileUrl: 'https://github.com/acme/skills/blob/main/skills/deploy/SKILL.md', modifiedAt: 1_790_000_000 }, +} + +describe('presentSkillDetail', () => { + it('returns each behavior with its path and line, without the page links', () => { + expect(presentSkillDetail(detail).behaviors).toEqual([ + { id: 'remote-code', tier: 'ask', label: 'Runs code downloaded from the network', locations: [{ path: 'SKILL.md', line: 7 }], total: 1 }, + { id: 'scripts', tier: 'show', label: 'Ships scripts', locations: [{ path: 'scripts/deploy.sh', line: null }], total: 1 }, + ]) + }) + + it('answers in the strict shape the contract publishes', () => { + const answer = presentSkillDetail(detail) + expect(skillsV1.operations.get.response.body.producer.parse(answer)).toEqual(answer) + }) +}) From 304290a2768c82879e2018c29d3d94fe4b6a51aa Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 7 Oct 2026 16:10:04 +1100 Subject: [PATCH 2/2] chore(deps): pin skilld-sdk 0.2.0 for Skill behaviors skilld 3.6.5 published skilld-sdk 0.2.0, whose skills.get contract requires the behaviors this branch now fills. --- pnpm-lock.yaml | 12 ++++++------ pnpm-workspace.yaml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8f2b901..8fab18f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -345,8 +345,8 @@ catalogs: specifier: ^2.5.3 version: 2.5.3 skilld-sdk: - specifier: 0.1.2 - version: 0.1.2 + specifier: 0.2.0 + version: 0.2.0 tailwindcss: specifier: ^4.3.3 version: 4.3.3 @@ -508,7 +508,7 @@ importers: version: 2.5.3 skilld-sdk: specifier: 'catalog:' - version: 0.1.2 + version: 0.2.0 tailwindcss: specifier: 'catalog:' version: 4.3.3 @@ -8632,8 +8632,8 @@ packages: resolution: {integrity: sha512-FZ7+DJe4DM+YNzNLlG5aLo/NY88Ie0pkO6um7l2NdWVUUvQWGhgTdUaYyYRPQVzK2e15PoChYvYyKMCtsrAl8g==} engines: {node: '>=22.0.0'} - skilld-sdk@0.1.2: - resolution: {integrity: sha512-nFo5TfDcMn8dRAM700L0nF3vwm5r/EENHBRNcNty2+OMXATNQTKU6VWOCkb3xIP2BGRtvbCI9maoV4vhgQKMYw==} + skilld-sdk@0.2.0: + resolution: {integrity: sha512-833JdLtIqknUF/EMQBQD50I4jHuVBI2I/Ly56+Ul+a8r+DsQMxl3Tu01MZ3JxrM712etdtfXzAn27NmZiRzMpQ==} slash@5.1.0: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} @@ -19369,7 +19369,7 @@ snapshots: dependencies: zod: 4.5.4 - skilld-sdk@0.1.2: + skilld-sdk@0.2.0: dependencies: zod: 4.5.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3f814b7b..26aa2b76 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -45,7 +45,7 @@ minimumReleaseAgeExclude: - nuxt-skew-protection@1.5.2 - '@nuxtjs/seo@5.3.14' - skilld-protocol@2.5.3 - - skilld-sdk@0.1.2 + - skilld-sdk@0.2.0 - skilld - skilld-cli-darwin-arm64 - skilld-cli-darwin-x64 @@ -291,7 +291,7 @@ catalog: # @nuxt/ui pins, or the client ships two copies. reka-ui: 2.10.4 skilld-protocol: ^2.5.3 - skilld-sdk: 0.1.2 + skilld-sdk: 0.2.0 tailwindcss: ^4.3.3 tsx: ^4.23.13 typescript: 6.0.3