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
6 changes: 3 additions & 3 deletions VISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ The filter for skilld-scoped work, precise enough to hold a proposal next to it

### 1. Provenance is the quality signal

A Skill is trustworthy because you can see who wrote it and read it before you run it. Every surface that shows a Skill names the author and links the exact source file in its repository. Registry identity is frozen at admission (ADR-0002) so provenance URLs never rot. Machine-generated metadata is labeled as such and never dressed as authorship.
A Skill is trustworthy because you can see who wrote it and read it before you run it. Every surface that shows a Skill names the author. The Skill page links the exact source file in its repository; other surfaces link the Skill page, which is where a visitor inspects a Skill. Registry identity is frozen at admission (ADR-0002) so provenance URLs never rot. Machine-generated metadata is labeled as such and never dressed as authorship.

**How to apply:** no Skill card, search result, or digest entry ships without author and source link. A layout that drops provenance to save space fails review. Trust signals (official repository, owner-verified) are earned facts, never editorial favors.
**How to apply:** no Skill card, search result, or digest entry ships without its author and a link to its Skill page or its source. A layout that drops provenance to save space fails review. Trust signals (official repository, owner-verified) are earned facts, never editorial favors.

**Test:** from any surface showing a Skill, can the user reach the exact SKILL.md in the author's repository in one click? If not, fail.
**Test:** from any surface showing a Skill, can the user reach its Skill page in one click, and the exact SKILL.md in the author's repository from there? If not, fail.

### 2. Curation over coverage

Expand Down
57 changes: 43 additions & 14 deletions layers/registry/server/data/skill-demos.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
"outputFile": "index.html",
"shots": [
{
"file": "desktop.jpg",
"file": "desktop-9954800c7c.jpg",
"width": 1440,
"height": 4102,
"viewport": "desktop",
"alt": "Desktop screenshot of the page the Agent built for: Build a single-file landing page for Tidepool, a tide-chart app for surfers. Inline all CSS and JavaScript. Save it as i"
},
{
"file": "mobile.jpg",
"file": "mobile-c67e258443.jpg",
"width": 390,
"height": 5091,
"viewport": "mobile",
Expand All @@ -43,7 +43,7 @@
"outputFile": "diagram.html",
"shots": [
{
"file": "desktop.jpg",
"file": "desktop-bb59621956.jpg",
"width": 1440,
"height": 927,
"viewport": "desktop",
Expand All @@ -66,14 +66,14 @@
"outputFile": "toast.html",
"shots": [
{
"file": "desktop.jpg",
"file": "desktop-9263d30c37.jpg",
"width": 1440,
"height": 900,
"viewport": "desktop",
"alt": "Desktop screenshot of the page the Agent built for: Build toast.html, a single-file HTML demo of a toast notification stack for Pebblequill, a fictional note-sync app. Show"
},
{
"file": "mobile.jpg",
"file": "mobile-578712c2dd.jpg",
"width": 390,
"height": 844,
"viewport": "mobile",
Expand All @@ -96,15 +96,15 @@
"skillCommit": "69a2169bde0c134f42979e1911755084b1c29962",
"recordedAt": "2026-10-06",
"video": {
"file": "video.mp4",
"poster": "poster.jpg",
"file": "video-77ba3f4e79.mp4",
"poster": "poster-9c7931fe85.jpg",
"width": 1280,
"height": 720,
"durationSeconds": 12
},
"shots": [
{
"file": "poster.jpg",
"file": "poster-9c7931fe85.jpg",
"width": 1280,
"height": 720,
"viewport": "desktop",
Expand All @@ -125,7 +125,7 @@
"outputFile": "flow.html",
"shots": [
{
"file": "desktop.jpg",
"file": "desktop-70a1bca51d.jpg",
"width": 1440,
"height": 1060,
"viewport": "desktop",
Expand All @@ -148,14 +148,14 @@
"outputFile": "pricing.html",
"shots": [
{
"file": "desktop.jpg",
"file": "desktop-4dce07cf5a.jpg",
"width": 1440,
"height": 1164,
"viewport": "desktop",
"alt": "Desktop screenshot of the page the Agent built for: Build pricing.html, a single-file HTML pricing section for Glintwork, a fictional photo-editing app: three plan cards, a"
},
{
"file": "mobile.jpg",
"file": "mobile-7180f99b12.jpg",
"width": 390,
"height": 2192,
"viewport": "mobile",
Expand All @@ -179,22 +179,51 @@
"skillCommit": "c893c5ed52aed84e3e2ee56787de869fccdae6b0",
"recordedAt": "2026-10-06",
"video": {
"file": "video.mp4",
"poster": "poster.jpg",
"file": "video-0683f0bd0b.mp4",
"poster": "poster-08e839d069.jpg",
"width": 1280,
"height": 720,
"durationSeconds": 21
},
"shots": [
{
"file": "poster.jpg",
"file": "poster-08e839d069.jpg",
"width": 1280,
"height": 720,
"viewport": "desktop",
"alt": "A frame from the video the Agent rendered for: /brag: make a short launch video for this project. Save the final video as launch.mp4 in this folder."
}
],
"pin": 1
},
{
"owner": "lemomo-ai",
"repo": "lemo-opuscar",
"name": "lemo-opuscar",
"authorName": "Lemomo",
"sourceUrl": "https://github.com/lemomo-ai/lemo-opuscar/blob/main/plugin/skills/lemo-opuscar/SKILL.md",
"prompt": "Make a 15-second film in the Chinese Ink Wash style about a lighthouse keeper on Wrenhollow, a fictional island, who rescues a paper boat from a storm. No narration. Render the final film as film.mp4 in this folder.",
"agent": "Claude Code",
"agentVersion": "2.1.291",
"model": "claude-opus-5-5",
"skillCommit": "02dce5b6ff6210622788c15319c082e9cb9e7217",
"recordedAt": "2026-10-06",
"video": {
"file": "video-b200a41390.mp4",
"poster": "poster-7f097c6c5c.jpg",
"width": 1280,
"height": 720,
"durationSeconds": 15
},
"shots": [
{
"file": "poster-7f097c6c5c.jpg",
"width": 1280,
"height": 720,
"viewport": "desktop",
"alt": "A frame from the video the Agent rendered for: Make a 15-second film in the Chinese Ink Wash style about a lighthouse keeper on Wrenhollow, a fictional island, who res"
}
]
}
]
}
36 changes: 21 additions & 15 deletions layers/registry/server/utils/skill-demos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ import manifest from '../data/skill-demos.json'
* Demos: one recorded run of a Skill each (GLOSSARY "demo").
*
* `scripts/record-skill-demo.ts` runs a fixed prompt through an Agent with the
* Skill loaded, screenshots the output into `public/demos/`, keeps the output
* page in `server/demos/`, and writes the entry below. Merging the pull request
* that adds an entry is the human approval: nothing records into production.
* Skill loaded, uploads the screenshots (or a video and its poster) to the
* `skilld-demo-media` R2 bucket, keeps the output page in `server/demos/`, and
* writes the entry below. Merging the pull request that adds an entry is the
* human approval: the media sits unreferenced until then.
*
* A video Skill's demo also carries the rendered video and a poster frame.
* Media lives at `DEMO_MEDIA_ORIGIN`, the bucket's custom domain, under
* content-hashed names, so a URL never changes what it serves.
*
* Cull path: delete the entry, its `public/demos/<owner>/<repo>/<name>/`
* folder, and its `server/demos/<owner>/<repo>/<name>/` folder.
* Cull path: delete the entry, its `server/demos/<owner>/<repo>/<name>/`
* folder, and the bucket's `demos/<owner>/<repo>/<name>/` prefix.
*/

const shotSchema = z.object({
/** File name inside `public/demos/<owner>/<repo>/<name>/`. A raster image only: an SVG can carry script. */
/** Content-hashed file name under the bucket's `demos/<owner>/<repo>/<name>/`. A raster image only: an SVG can carry script. */
file: z.string().regex(/^[\w-]+\.(?:png|jpg)$/),
width: z.number().int().positive(),
height: z.number().int().positive(),
Expand All @@ -26,7 +28,7 @@ const shotSchema = z.object({
})

const videoSchema = z.object({
/** H.264 MP4 inside `public/demos/<owner>/<repo>/<name>/`, re-encoded small for the web. */
/** Content-hashed H.264 MP4 under the bucket's `demos/<owner>/<repo>/<name>/`, re-encoded small for the web. */
file: z.string().regex(/^[\w-]+\.mp4$/),
/** A JPG frame shown before the video plays. */
poster: z.string().regex(/^[\w-]+\.jpg$/),
Expand Down Expand Up @@ -83,7 +85,10 @@ export function listSkillDemos(demos: readonly SkillDemoRecord[] = SKILL_DEMOS):
|| b.recordedAt.localeCompare(a.recordedAt))
}

function demoBase(demo: SkillDemoRecord): string {
/** The `skilld-demo-media` bucket's custom domain. */
export const DEMO_MEDIA_ORIGIN = 'https://media.skilld.dev'

function demoPath(demo: SkillDemoRecord): string {
return `/demos/${demo.owner}/${demo.repo}/${demo.name}`
}

Expand Down Expand Up @@ -126,8 +131,9 @@ export interface SkillDemoView {
}

/** `currentCommit` is the Skill's source commit now, when known. Unknown never marks a demo outdated. */
export function presentSkillDemo(demo: SkillDemoRecord, currentCommit: string | null): SkillDemoView {
const base = demoBase(demo)
export function presentSkillDemo(demo: SkillDemoRecord, currentCommit: string | null, mediaOrigin: string = DEMO_MEDIA_ORIGIN): SkillDemoView {
const path = demoPath(demo)
const media = `${mediaOrigin}${path}`
return {
owner: demo.owner,
repo: demo.repo,
Expand All @@ -142,18 +148,18 @@ export function presentSkillDemo(demo: SkillDemoRecord, currentCommit: string |
skillCommit: demo.skillCommit,
recordedAt: demo.recordedAt,
outdated: currentCommit !== null && currentCommit !== demo.skillCommit,
liveUrl: demo.outputFile ? `${base}/live` : null,
liveUrl: demo.outputFile ? `${path}/live` : null,
video: demo.video
? {
src: `${base}/${demo.video.file}`,
poster: `${base}/${demo.video.poster}`,
src: `${media}/${demo.video.file}`,
poster: `${media}/${demo.video.poster}`,
width: demo.video.width,
height: demo.video.height,
durationSeconds: demo.video.durationSeconds,
}
: null,
shots: demo.shots.map(shot => ({
src: `${base}/${shot.file}`,
src: `${media}/${shot.file}`,
width: shot.width,
height: shot.height,
alt: shot.alt,
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed public/demos/humanlayer/skills/show-me/desktop.jpg
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed public/demos/latent-spaces/brag/brag/poster.jpg
Binary file not shown.
Binary file removed public/demos/latent-spaces/brag/brag/video.mp4
Binary file not shown.
82 changes: 65 additions & 17 deletions scripts/record-skill-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
* 3. An HTML output gets full-page screenshots at desktop width, and at phone
* width when the page fits a phone. An MP4 output is re-encoded small and
* gets a poster frame.
* 4. Copies the media to `public/demos/`, an HTML page to the registry's
* 4. Uploads the media to the `skilld-demo-media` R2 bucket with `cf`, under
* content-hashed names, copies an HTML page to the registry's
* `server/demos/`, and upserts the entry in `server/data/skill-demos.json`.
* The account comes from `CLOUDFLARE_ACCOUNT_ID`, else `wrangler.jsonc`.
*
* A page Skill gets only Read, Write and Edit. A video Skill has to run its
* renderer, so its Bash runs in the Claude Code sandbox: writes stay in the temp
Expand All @@ -29,9 +31,10 @@
*/

import { execFile } from 'node:child_process'
import { createHash } from 'node:crypto'
import { copyFile, cp, mkdir, mkdtemp, open, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { homedir, tmpdir } from 'node:os'
import { extname, join, resolve } from 'node:path'
import { extname, join, relative, resolve, sep } from 'node:path'
import process from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { parseArgs, promisify } from 'node:util'
Expand All @@ -42,7 +45,8 @@ const run = promisify(execFile)
const ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)))
const MANIFEST = join(ROOT, 'layers/registry/server/data/skill-demos.json')
const OUTPUT_DIR = join(ROOT, 'layers/registry/server/demos')
const SHOT_DIR = join(ROOT, 'public/demos')
const MEDIA_BUCKET = 'skilld-demo-media'
const MEDIA_TYPES: Readonly<Record<string, string>> = { '.jpg': 'image/jpeg', '.png': 'image/png', '.mp4': 'video/mp4' }
const API = 'https://skilld.dev/api/v1/skills'
const RECORD_TIMEOUT_MS = 20 * 60 * 1000
/** Rendering a film takes far longer than writing a page. */
Expand Down Expand Up @@ -290,7 +294,11 @@ async function encodeVideo(source: string, dir: string, prompt: string): Promise
}
}

/** The named file, or the newest file of the same type anywhere in the folder. */
/**
* The named file, or the newest file of the same type in the folder. Hidden
* folders and node_modules hold renderer scratch, such as the segments a film
* is cut from, so they never count as the output.
*/
async function findOutput(cwd: string, output: string): Promise<string | null> {
const named = join(cwd, output)
const found = await stat(named).catch((error: NodeJS.ErrnoException) => {
Expand All @@ -304,7 +312,8 @@ async function findOutput(cwd: string, output: string): Promise<string | null> {
const wanted = extname(output)
const entries = await readdir(cwd, { recursive: true, withFileTypes: true })
const matches = await Promise.all(entries
.filter(entry => entry.isFile() && extname(entry.name) === wanted && !entry.parentPath.includes('node_modules'))
.filter(entry => entry.isFile() && extname(entry.name) === wanted
&& !relative(cwd, entry.parentPath).split(sep).some(part => part.startsWith('.') || part === 'node_modules'))
.map(async (entry) => {
const path = join(entry.parentPath, entry.name)
return { path, mtime: (await stat(path)).mtimeMs }
Expand Down Expand Up @@ -349,6 +358,49 @@ async function writeEntry(entry: DemoEntry): Promise<void> {
await writeFile(MANIFEST, `${JSON.stringify({ demos }, null, 2)}\n`)
}

async function cloudflareAccount(): Promise<string> {
if (process.env.CLOUDFLARE_ACCOUNT_ID)
return process.env.CLOUDFLARE_ACCOUNT_ID
const config = await readFile(join(ROOT, 'wrangler.jsonc'), 'utf8')
const account = /"account_id":\s*"([0-9a-f]{32})"/.exec(config)?.[1]
if (!account)
throw new Error('Set CLOUDFLARE_ACCOUNT_ID: wrangler.jsonc has no account_id.')
return account
}

/**
* Uploads each staged file under a content-hashed name, so a media URL never
* changes what it serves, and returns the old name to new name map.
*/
async function publishMedia(owner: string, repo: string, name: string, dir: string, files: string[]): Promise<Map<string, string>> {
const account = await cloudflareAccount()
const published = new Map<string, string>()
for (const file of files) {
const local = join(dir, file)
const ext = extname(file)
const hash = createHash('sha256').update(await readFile(local)).digest('hex').slice(0, 10)
const hashed = `${file.slice(0, -ext.length)}-${hash}${ext}`
const key = `demos/${owner}/${repo}/${name}/${hashed}`
await run('cf', ['r2', 'objects', 'put', key, '--bucket-name', MEDIA_BUCKET, '--file', local, '--content-type', MEDIA_TYPES[ext] ?? 'application/octet-stream', '--quiet'], {
env: { ...process.env, CLOUDFLARE_ACCOUNT_ID: account },
maxBuffer: 16 * 1024 * 1024,
})
published.set(file, hashed)
}
return published
}

/** Stages, uploads, and renames the media of one demo. */
async function publishDemoMedia(owner: string, repo: string, name: string, dir: string, media: Pick<DemoEntry, 'video' | 'shots'>): Promise<Pick<DemoEntry, 'video' | 'shots'>> {
const files = [...new Set([...media.shots.map(shot => shot.file), ...(media.video ? [media.video.file, media.video.poster] : [])])]
const names = await publishMedia(owner, repo, name, dir, files)
const to = (file: string) => names.get(file) ?? file
return {
shots: media.shots.map(shot => ({ ...shot, file: to(shot.file) })),
...(media.video ? { video: { ...media.video, file: to(media.video.file), poster: to(media.video.poster) } } : {}),
}
}

async function reshoot(owner: string, repo: string, name: string): Promise<void> {
const manifest = JSON.parse(await readFile(MANIFEST, 'utf8')) as { demos: DemoEntry[] }
const ref = `${owner}/${repo}/${name}`.toLowerCase()
Expand All @@ -357,11 +409,9 @@ async function reshoot(owner: string, repo: string, name: string): Promise<void>
throw new Error(`No recorded demo for ${owner}/${repo}/${name}. Record it first.`)
if (!entry.outputFile)
throw new Error(`${owner}/${repo}/${name} is a video demo with no page to reshoot. Record it again instead.`)
const shotDir = join(SHOT_DIR, entry.owner, entry.repo, entry.name)
await rm(shotDir, { recursive: true, force: true })
await mkdir(shotDir, { recursive: true })
const stage = await mkdtemp(join(tmpdir(), 'skilld-demo-media-'))
const page = join(OUTPUT_DIR, entry.owner, entry.repo, entry.name, entry.outputFile)
const shots = await screenshot(page, shotDir, entry.prompt)
const { shots } = await publishDemoMedia(entry.owner, entry.repo, entry.name, stage, { shots: await screenshot(page, stage, entry.prompt) })
// The recorded commit stays; only the provenance fields refresh.
const source = await skillSource(entry.owner, entry.repo, entry.name)
await upsert({ ...entry, authorName: source.authorName, sourceUrl: source.sourceUrl, shots })
Expand Down Expand Up @@ -393,18 +443,16 @@ async function main(): Promise<void> {
if (!produced)
throw new Error(`The Agent wrote no ${extname(output)} file. Check ${cwd}.`)

const shotDir = join(SHOT_DIR, owner, repo, name)
await rm(shotDir, { recursive: true, force: true })
await mkdir(shotDir, { recursive: true })

const media: Pick<DemoEntry, 'outputFile' | 'video' | 'shots'> = kind === 'video'
? await encodeVideo(produced, shotDir, prompt)
const stage = await mkdtemp(join(tmpdir(), 'skilld-demo-media-'))
const staged: Pick<DemoEntry, 'outputFile' | 'video' | 'shots'> = kind === 'video'
? await encodeVideo(produced, stage, prompt)
: await (async () => {
const outputDir = join(OUTPUT_DIR, owner, repo, name)
await mkdir(outputDir, { recursive: true })
await copyFile(produced, join(outputDir, output))
return { outputFile: output, shots: await screenshot(produced, shotDir, prompt) }
return { outputFile: output, shots: await screenshot(produced, stage, prompt) }
})()
const media = { ...staged, ...await publishDemoMedia(owner, repo, name, stage, staged) }

await upsert({
owner,
Expand All @@ -421,7 +469,7 @@ async function main(): Promise<void> {
recordedAt: new Date().toISOString().slice(0, 10),
...media,
})
console.log(`Recorded ${skillRef} with ${agent.model}. Review ${shotDir} before you commit. The Agent's folder stays at ${cwd}.`)
console.log(`Recorded ${skillRef} with ${agent.model}. Review the media in ${stage} before you commit. The Agent's folder stays at ${cwd}.`)
}

await main()
Loading