Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions COPY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions layers/registry/app/components/_SkillCommandPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ function copyFrom(next: CommandMode) {

<div class="space-y-1 border-t border-default pt-6">
<NuxtLink
:to="`${runUrl}.md`"
:to="`${runUrl}.md?action=fork`"
class="inline-flex min-h-11 items-center font-mono text-sm text-default underline underline-offset-2 hover:text-primary"
>
Fork this Skill
Expand Down Expand Up @@ -289,7 +289,7 @@ function copyFrom(next: CommandMode) {

<div class="space-y-1">
<NuxtLink
:to="`${runUrl}.md`"
:to="`${runUrl}.md?action=fork`"
class="inline-flex min-h-11 items-center font-mono text-sm text-default underline underline-offset-2 hover:text-primary"
>
Fork this Skill
Expand Down
39 changes: 31 additions & 8 deletions layers/registry/server/utils/skill-agent-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export function composeAgentSkillMarkdown(input: AgentSkillMarkdownInput): strin
.join('\n\n')
}

export function composeSkillForkMarkdown(input: Pick<AgentSkillMarkdownInput, 'owner' | 'repo' | 'name'>): 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.',
Expand All @@ -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<AgentSkillMarkdownInput, 'owner' | 'repo' | 'name'>): 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 {
Expand Down
17 changes: 10 additions & 7 deletions server/plugins/ai-ready-markdown-source.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions skills/skilld-registry/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <skilld.dev URL>`, fetch that page with `Accept: text/markdown`.
If the user says `fork this Skill <skilld.dev URL>`, 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.
Expand Down
17 changes: 14 additions & 3 deletions test/unit/agent-skill-markdown-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}

Expand All @@ -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 (
Expand Down Expand Up @@ -52,12 +52,23 @@ async function requestMarkdown(route: string, skills: { name: string, resolved?:
registerPlugin(app as unknown as Parameters<typeof registerPlugin>[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 }])

Expand Down
2 changes: 1 addition & 1 deletion test/unit/skill-command-panel-setup-link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
})

Expand Down