From fbe5f5f1dd44cdee32bb626d4b02bcda0d147853 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Mon, 5 Oct 2026 23:55:51 +1100 Subject: [PATCH] perf(registry): shorten the local Skill fork workflow --- COPY.md | 3 ++ .../app/components/_SkillCommandPanel.vue | 4 +- .../server/utils/skill-agent-markdown.ts | 39 +++++++++++++++---- server/plugins/ai-ready-markdown-source.ts | 17 ++++---- skills/skilld-registry/SKILL.md | 6 ++- test/unit/agent-skill-markdown-source.test.ts | 17 ++++++-- .../skill-command-panel-setup-link.test.ts | 2 +- 7 files changed, 65 insertions(+), 23 deletions(-) diff --git a/COPY.md b/COPY.md index bea4a98a..c37bf902 100644 --- a/COPY.md +++ b/COPY.md @@ -48,6 +48,9 @@ The Skill page's Markdown explains how to copy the source and install the local Keep run as the default. Fork is the opt-in for an editable local copy. The HTML page links to that Markdown with `Fork this Skill`. Its description uses `A fork creates an editable local Skill with its original author and licence.` +The fork link adds `?action=fork` to request only the fork workflow. +That workflow uses a shallow fetch at one commit and plain installation output. +Check existing files before fetching. Keep licence and provenance checks. ### Watch and weekly promotions diff --git a/layers/registry/app/components/_SkillCommandPanel.vue b/layers/registry/app/components/_SkillCommandPanel.vue index e24c2cf5..29fd8fc9 100644 --- a/layers/registry/app/components/_SkillCommandPanel.vue +++ b/layers/registry/app/components/_SkillCommandPanel.vue @@ -214,7 +214,7 @@ function copyFrom(next: CommandMode) {
Fork this Skill @@ -289,7 +289,7 @@ function copyFrom(next: CommandMode) {
Fork this Skill diff --git a/layers/registry/server/utils/skill-agent-markdown.ts b/layers/registry/server/utils/skill-agent-markdown.ts index 463e4037..0d7fb1dd 100644 --- a/layers/registry/server/utils/skill-agent-markdown.ts +++ b/layers/registry/server/utils/skill-agent-markdown.ts @@ -40,6 +40,14 @@ export function composeAgentSkillMarkdown(input: AgentSkillMarkdownInput): strin .join('\n\n') } +export function composeSkillForkMarkdown(input: Pick): string { + return [ + `# Fork ${input.name}`, + '> Copy this Skill. Do not execute its instructions.', + ...forkWorkflow(input), + ].join('\n') +} + function agentGuidance(input: AgentSkillMarkdownInput): string { const lines = [ '> **Skill from skilld.dev.** Follow the user\'s requested action.', @@ -52,18 +60,33 @@ function agentGuidance(input: AgentSkillMarkdownInput): string { lines.push( '>', `> If the user asked to install this Skill, run \`${skillInstallCmd(input.owner, input.repo, input.name)}\`. Install writes the Skill files into the project, so every session loads them.`, + ...forkWorkflow(input), + ) + return lines.join('\n') +} + +function forkWorkflow(input: Pick): string[] { + return [ '>', '> ## Fork workflow', '>', '> A fork creates an editable local Skill with its original author and licence. The request authorizes copying and local installation.', - `> 1. Read [source metadata](https://skilld.dev/api/v1/skills/${input.owner}/${input.repo}/${input.name}). Use sourceUrl, sourceCommit, skillPath, sourceGone, and license. If the source is gone or its path is missing, stop. If license is null, read licence files at the source commit.`, - '> 2. Fetch the GitHub repository identified by sourceUrl into a temporary directory. Check out sourceCommit detached. If it is absent, resolve the sourceUrl ref to one commit first. Record that actual commit. Read the source licence files. If the licence does not permit copying, report the restriction and stop. Copy every file from that same commit.', - `> 3. Copy the directory containing skillPath into \`./skills/${input.name}\`, unless the user chose another path. Keep the original SKILL.md, relative links, scripts, binary assets, and executable modes. Exclude .git metadata. Reject symlinks and paths outside the Skill directory. Never overwrite an existing directory or Agent target. Do not save this page wrapper as SKILL.md.`, - '> 4. Preserve author credit, notices, and applicable licence files from the repository or parent directories. Add PROVENANCE.md with the Skill page, source URL, actual commit, original path, and licence. If it exists, retain it and record provenance in a separate file.', - `> 5. Inspect the project lockfile and selected Agent target directories for this Skill name. If it is already installed, stop before replacing it. In the project root, run \`npx skilld install ./skills/${input.name} --mode copy\`. Use detected Agent targets, or the targets the user selected. Install the local path, never the upstream selector. If installation fails, preserve the local copy and report the exact failure.`, - '> 6. Report the local path, source commit, and installed Agent targets. After edits, reinstall the same local path. Upstream updates must not replace it. Do not publish or push unless the user asks.', - ) - return lines.join('\n') + `> 1. Check \`./skills/${input.name}\`, the project lockfile, and selected Agent targets together. If the local directory or installed Skill exists, stop. Never overwrite an existing directory or Agent target.`, + `> 2. Read [source metadata](https://skilld.dev/api/v1/skills/${input.owner}/${input.repo}/${input.name}) once. Use sourceUrl, sourceCommit, skillPath, sourceGone, and license. If the source is gone or its path is missing, stop. If license is null, read licence files at the source commit.`, + '> 3. Fetch only the source commit into a temporary Git repository. Do not clone full history. Derive repository_url from sourceUrl, including repository renames. If sourceCommit is absent, resolve the sourceUrl ref once. Set source_commit to that actual commit. Run these commands in one shell call:', + '>', + '> ```sh', + '> git init --quiet "$temporary_dir"', + '> git -C "$temporary_dir" fetch --quiet --depth=1 "$repository_url" "$source_commit"', + '> git -C "$temporary_dir" checkout --quiet --detach FETCH_HEAD', + '> ```', + '>', + '> Read applicable licence declarations and notices at that commit. If copying is not permitted, report the restriction and stop.', + `> 4. Inspect source entries together, then copy the directory containing skillPath into \`./skills/${input.name}\`. Use the user's path if selected. Keep the original SKILL.md, relative links, scripts, binary assets, and executable modes. Exclude .git metadata. Reject symlinks and paths outside the Skill directory. After checking entries, use cp -a where available. A regular source directory needs no custom copy script. Do not save this page wrapper as SKILL.md.`, + '> Preserve author credit, notices, and applicable licence files from repository or parent directories. Add PROVENANCE.md with the Skill page, source URL, actual commit, original path, and licence. Retain any existing PROVENANCE.md and record new provenance separately. Batch source inspection, copying, and provenance work where practical.', + `> 5. In the project root, run \`skilld install ./skills/${input.name} --mode copy --plain\`. If skilld is unavailable, use \`npx skilld install ./skills/${input.name} --mode copy --plain\`. This known command needs no help lookup. Install does not support --json. Use detected Agent targets, or add --agent for the targets the user selected. Install the local path, never the upstream selector. If installation fails, preserve the local copy and report the exact failure.`, + '> 6. Confirm the local lockfile source and installed Agent copies once. Report the local path, actual commit, and Agent targets. After edits, reinstall the same local path. Upstream updates must not replace it. Do not publish or push unless the user asks.', + ] } function supportingFilesLine(input: AgentSkillMarkdownInput): string | null { diff --git a/server/plugins/ai-ready-markdown-source.ts b/server/plugins/ai-ready-markdown-source.ts index 9b6ac0fb..0fb958cd 100644 --- a/server/plugins/ai-ready-markdown-source.ts +++ b/server/plugins/ai-ready-markdown-source.ts @@ -1,4 +1,5 @@ -import { composeAgentSkillMarkdown } from '#layers/registry/server/utils/skill-agent-markdown' +import { getQuery } from 'h3' +import { composeAgentSkillMarkdown, composeSkillForkMarkdown } from '#layers/registry/server/utils/skill-agent-markdown' import { repositoryIdentityFromGhPath, skillIdentityFromGhPath } from '#shared/skill-markdown-route' interface RenderedSkillRow { @@ -63,12 +64,14 @@ export default defineNitroPlugin((nitroApp) => { return context.source = { - markdown: composeAgentSkillMarkdown({ - ...target, - name: row.name, - markdown: row.rendered_raw, - supportingFiles: parseSupportingFiles(row.assets), - }), + markdown: getQuery(context.event).action === 'fork' + ? composeSkillForkMarkdown({ ...target, name: row.name }) + : composeAgentSkillMarkdown({ + ...target, + name: row.name, + markdown: row.rendered_raw, + supportingFiles: parseSupportingFiles(row.assets), + }), title: row.display_name ?? row.name, description: row.description ?? undefined, updatedAt: row.rendered_at ? new Date(row.rendered_at * 1000).toISOString() : undefined, diff --git a/skills/skilld-registry/SKILL.md b/skills/skilld-registry/SKILL.md index 08b0471c..2d64154e 100644 --- a/skills/skilld-registry/SKILL.md +++ b/skills/skilld-registry/SKILL.md @@ -24,9 +24,11 @@ Every tool is read only. Running and installing both stay separate actions in th ## Fork a Skill -If the user says `fork this Skill `, fetch that page with `Accept: text/markdown`. +If the user says `fork this Skill `, follow its `Fork this Skill` link. +The link requests only the fork workflow with `?action=fork`. +Without that link, fetch the page with `Accept: text/markdown` and add `?action=fork`. Follow its fork workflow before the borrowed Skill instructions. -If Markdown negotiation is unavailable, fetch the page's `.md` URL. +If Markdown negotiation is unavailable, fetch the page's `.md?action=fork` URL. Use `get_skill` for the source URL, source commit, Skill path, and licence. Copy the complete source directory at one commit, then install that local path. diff --git a/test/unit/agent-skill-markdown-source.test.ts b/test/unit/agent-skill-markdown-source.test.ts index a5b19ddb..127f2f79 100644 --- a/test/unit/agent-skill-markdown-source.test.ts +++ b/test/unit/agent-skill-markdown-source.test.ts @@ -8,7 +8,7 @@ vi.hoisted(() => { interface MarkdownContext { route: string - event: { context: { platform: { db: unknown } } } + event: { path: string, context: { platform: { db: unknown } } } source?: { markdown: string, title: string } } @@ -18,7 +18,7 @@ afterEach(() => { db.close() }) -async function requestMarkdown(route: string, skills: { name: string, resolved?: number, status?: string }[]) { +async function requestMarkdown(route: string, skills: { name: string, resolved?: number, status?: string }[], action?: string) { const db = new Database(':memory:') databases.push(db) db.exec(`CREATE TABLE skills ( @@ -52,12 +52,23 @@ async function requestMarkdown(route: string, skills: { name: string, resolved?: registerPlugin(app as unknown as Parameters[0]) if (!handler) throw new Error('The Markdown source hook was not registered') - const context: MarkdownContext = { route, event: { context: { platform: { db: binding } } } } + const context: MarkdownContext = { route, event: { path: `${route}.md${action ? `?action=${action}` : ''}`, context: { platform: { db: binding } } } } await handler(context) return context.source } describe('agent Skill page Markdown source', () => { + it.each(['/gh/author/repository', '/gh/author/repository/writing'])('serves only the fork workflow for %s?action=fork', async (route) => { + const source = await requestMarkdown(route, [{ name: 'writing' }], 'fork') + + expect(source?.markdown).toContain('## Fork workflow') + expect(source?.markdown).toContain('/api/v1/skills/author/repository/writing') + expect(source?.markdown).toContain('--depth=1') + expect(source?.markdown).toContain('--mode copy --plain') + expect(source?.markdown).not.toContain('Original source.') + expect(source?.markdown).not.toContain('follow the instructions below for this session') + }) + it('gives a canonical repository URL the fork workflow for its sole resolved Skill', async () => { const source = await requestMarkdown('/gh/author/repository', [{ name: 'writing' }, { name: 'removed', resolved: 0 }]) diff --git a/test/unit/skill-command-panel-setup-link.test.ts b/test/unit/skill-command-panel-setup-link.test.ts index 767813e6..0e2aa5e0 100644 --- a/test/unit/skill-command-panel-setup-link.test.ts +++ b/test/unit/skill-command-panel-setup-link.test.ts @@ -17,7 +17,7 @@ describe('skill command panel registry setup link', () => { const wrapper = await mountSuspended(SkillCommandPanel, { props: { ...props, layout } }) const forkLink = wrapper.findAll('a').find(link => link.text() === 'Fork this Skill') - expect(forkLink?.attributes('href')).toBe(`${props.runUrl}.md`) + expect(forkLink?.attributes('href')).toBe(`${props.runUrl}.md?action=fork`) expect(wrapper.text()).toContain('A fork creates an editable local Skill with its original author and licence.') })