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
5 changes: 4 additions & 1 deletion packages/harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ An `InvalidSkill` error lists each check that still fails in `issues`.

The Harness checks the rules that the generation Skills state:

- The frontmatter contains only `name` and `description`.
- The frontmatter contains `name` and `description`, with optional `license`, `compatibility`, `metadata`, and `allowed-tools`.
- `license` is a non-empty string. `compatibility` contains 1 to 500 characters when supplied.
- Include `compatibility` only for specific environment requirements. Most Skills omit it.
- `metadata` maps string keys to string values. `allowed-tools` is an experimental space-separated string.
- The description is one plain YAML line without double quotes, backticks, or `%`.
- `SKILL.md` links every file under `references/`, by a Markdown link or an inline code path.
- A package Skill keeps `SKILL.md` under 500 lines and writes at most eight reference files.
Expand Down
34 changes: 20 additions & 14 deletions packages/harness/src/internal/output/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import type { Result } from '../result.ts'
import type { CollectedFile } from './collect.ts'
import { isScalar, parseDocument, Scalar } from 'yaml'
import { isSkillName, normalizeOutputPath } from '../paths.ts'
import { checkProjectSkill } from './project.ts'
import { err, ok } from '../result.ts'
import { checkProjectSkill } from './project.ts'

/** Both generation Skills say the frontmatter must contain only these fields. */
const allowedFrontmatter = new Set(['name', 'description'])
/** The six fields defined by the Agent Skills specification. */
const allowedFrontmatter = new Set(['name', 'description', 'license', 'compatibility', 'metadata', 'allowed-tools'])

/** `generate-package-skill`: "Keep `SKILL.md` under 500 lines." */
const packageSkillLineLimit = 500
Expand All @@ -29,8 +29,9 @@ export type GenerationContract
= | { readonly _tag: 'PackageSkill' }
| { readonly _tag: 'ProjectSkill', readonly projectPaths: ReadonlyArray<string> }

const invalid = (issues: ReadonlyArray<string>): Result<never, SkillRunError> =>
err({ _tag: 'InvalidSkill', message: 'Skill output failed deterministic checks.', issues })
function invalid(issues: ReadonlyArray<string>): Result<never, SkillRunError> {
return err({ _tag: 'InvalidSkill', message: 'Skill output failed deterministic checks.', issues })
}

function decodeText(content: Uint8Array): string | null {
try {
Expand Down Expand Up @@ -82,11 +83,7 @@ function structureIssues(markdown: string, files: ReadonlyArray<CollectedFile>,
return issues
}

export const validateGeneratedSkill = (
name: string,
files: ReadonlyArray<CollectedFile>,
contract: GenerationContract,
): Result<void, SkillRunError> => {
export function validateGeneratedSkill(name: string, files: ReadonlyArray<CollectedFile>, contract: GenerationContract): Result<void, SkillRunError> {
const issues: string[] = []
const skillFiles = files.filter(file => file.path === 'SKILL.md')
if (skillFiles.length !== 1)
Expand Down Expand Up @@ -129,8 +126,19 @@ export const validateGeneratedSkill = (
issues.push('Frontmatter name must match the Skill directory name.')
if (typeof frontmatter.name !== 'string' || !isSkillName(frontmatter.name))
issues.push('Frontmatter name is invalid.')
if (typeof frontmatter.description !== 'string' || frontmatter.description.trim().length === 0 || frontmatter.description.length > 1024)
if (typeof frontmatter.description !== 'string' || frontmatter.description.trim().length === 0 || [...frontmatter.description].length > 1024)
issues.push('Frontmatter description must contain 1 to 1024 characters.')
if ('license' in frontmatter && (typeof frontmatter.license !== 'string' || frontmatter.license.trim().length === 0))
issues.push('Frontmatter license must be a non-empty string.')
if ('compatibility' in frontmatter && (typeof frontmatter.compatibility !== 'string' || frontmatter.compatibility.trim().length === 0 || [...frontmatter.compatibility].length > 500))
issues.push('Frontmatter compatibility must contain 1 to 500 characters.')
if ('allowed-tools' in frontmatter && typeof frontmatter['allowed-tools'] !== 'string')
issues.push('Frontmatter allowed-tools must be a string.')
if ('metadata' in frontmatter) {
const metadata = document.toJS({ mapAsMap: true }).get('metadata') as unknown
if (!(metadata instanceof Map) || [...metadata].some(([key, value]) => typeof key !== 'string' || typeof value !== 'string'))
issues.push('Frontmatter metadata must map string keys to string values.')
}
const description = document.get('description', true)
if (typeof frontmatter.description === 'string' && (!isScalar(description) || description.type !== Scalar.PLAIN || riskyDescription.test(frontmatter.description)))
issues.push('Frontmatter description must be one plain line without double quotes, backticks, or %. Name symptoms in plain words.')
Expand All @@ -141,9 +149,7 @@ export const validateGeneratedSkill = (

const findingLevels = new Set(['error', 'warning', 'note'])

export const validateSkillReview = (
files: ReadonlyArray<CollectedFile>,
): Result<SkillReview, SkillRunError> => {
export function validateSkillReview(files: ReadonlyArray<CollectedFile>): Result<SkillReview, SkillRunError> {
if (files.length !== 1 || files[0]?.path !== 'review.json')
return invalid(['Review output must contain only review.json.'])

Expand Down
50 changes: 43 additions & 7 deletions packages/harness/test/e2e/output-checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,21 @@ async function runTurns(outputs: ReadonlyArray<Output>) {
}

describe('output check feedback', () => {
const invalid = { 'SKILL.md': `${frontmatter('example-package', 'license: MIT\n')}# Example\n` }
const invalid = { 'SKILL.md': `${frontmatter('example-package', 'unsupported: value\n')}# Example\n` }
const valid = { 'SKILL.md': `${frontmatter('example-package')}# Example\n` }

it('sends failed checks back to the Agent in the same session and promotes the repaired Skill', async () => {
const { result, prompts } = await runTurns([invalid, valid])

expect(result).toMatchObject({ _tag: 'Ok', value: { _tag: 'GeneratedSkill' } })
expect(prompts).toHaveLength(2)
expect(prompts[1]).toContain('Frontmatter field is not supported: license')
expect(prompts[1]).toContain('Frontmatter field is not supported: unsupported')
})

it('returns the remaining issues after two repair turns', async () => {
const { result, prompts } = await runTurns([invalid])

expect(issues(result)).toContain('Frontmatter field is not supported: license')
expect(issues(result)).toContain('Frontmatter field is not supported: unsupported')
expect(prompts).toHaveLength(3)
})
})
Expand All @@ -113,14 +113,51 @@ describe('generated Skill output checks', () => {
})

it.each([
['license: MIT\n', 'license'],
['metadata:\n owner: example\n', 'metadata'],
])('rejects a frontmatter field other than name and description (%s)', async (extra, field) => {
['unsupported: value\n', 'unsupported'],
])('rejects a frontmatter field outside the generation contract (%s)', async (extra, field) => {
const result = await runPackageSkill({ 'SKILL.md': `${frontmatter('example-package', extra)}# Example\n` })

expect(issues(result)).toContain(`Frontmatter field is not supported: ${field}`)
})

it.each([
'license: MIT\n',
'compatibility: Requires Node.js 22 or later.\n',
'license: MIT\ncompatibility: Requires Node.js 22 or later.\n',
`compatibility: "${'x'.repeat(500)}"\n`,
`compatibility: "${'πŸ˜€'.repeat(500)}"\n`,
'metadata:\n author: example\n version: "1.0"\n',
'allowed-tools: Read Bash(git:*)\n',
])('promotes a package Skill with optional metadata (%s)', async (extra) => {
const result = await runPackageSkill({ 'SKILL.md': `${frontmatter('example-package', extra)}# Example\n` })

expect(result).toMatchObject({ _tag: 'Ok', value: { _tag: 'GeneratedSkill' } })
})

it('promotes a project Skill with license and compatibility metadata', async () => {
const result = await runProjectSkill({
'SKILL.md': `${frontmatter('example-project', 'license: MIT\ncompatibility: Requires shell access.\n')}${projectSkillBody()}`,
})

expect(result).toMatchObject({ _tag: 'Ok', value: { _tag: 'GeneratedSkill' } })
})

it.each([
['license: 42\n', 'Frontmatter license must be a non-empty string.'],
['license: " "\n', 'Frontmatter license must be a non-empty string.'],
['compatibility: true\n', 'Frontmatter compatibility must contain 1 to 500 characters.'],
['compatibility: " "\n', 'Frontmatter compatibility must contain 1 to 500 characters.'],
[`compatibility: ${'x'.repeat(501)}\n`, 'Frontmatter compatibility must contain 1 to 500 characters.'],
['metadata: []\n', 'Frontmatter metadata must map string keys to string values.'],
['metadata:\n version: 1\n', 'Frontmatter metadata must map string keys to string values.'],
['metadata:\n 1: example\n', 'Frontmatter metadata must map string keys to string values.'],
['allowed-tools: [Read]\n', 'Frontmatter allowed-tools must be a string.'],
])('rejects invalid optional metadata (%s)', async (extra, issue) => {
const result = await runPackageSkill({ 'SKILL.md': `${frontmatter('example-package', extra)}# Example\n` })

expect(issues(result)).toContain(issue)
})

it('rejects a package SKILL.md of 500 lines or more', async () => {
const body = Array.from({ length: 500 }, (_, index) => `Line ${index + 1}.`).join('\n')
const result = await runPackageSkill({ 'SKILL.md': `${frontmatter('example-package')}${body}\n` })
Expand Down Expand Up @@ -176,7 +213,6 @@ describe('generated Skill output checks', () => {
})

expect(issues(result)).toEqual([
'Frontmatter field is not supported: license',
'SKILL.md must link references/orphan.md.',
])
})
Expand Down
8 changes: 7 additions & 1 deletion skills/generate-package-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
name: generate-package-skill
description: Generate or update an Agent Skill that teaches consumers one npm or local package the user maintains, including framework modules and wrappers. Tests each example against the installed version. Use when a maintainer asks for a package Skill, a SKILL.md for their library, or a Skill update after a release.
license: MIT
compatibility: "Requires an agent with filesystem and shell access. Network access is needed for remote sources."
---

# Generate a package Skill
Expand Down Expand Up @@ -100,7 +102,11 @@ Shape:
- Skillgen maintains only `SKILL.md` and Markdown under `references/`: at most 9 files and 64 KiB in total. It never updates `scripts/`.
- Never include credentials, caches, build output, or dependency directories.

The frontmatter contains only `name` and `description`.
The frontmatter contains `name` and `description`. It may also contain `license`, `compatibility`, `metadata`, and `allowed-tools`.
If the source declares a license, copy its identifier into `license`. Never infer a license.
Include `compatibility` only for specific tool, runtime, network, or product requirements, at most 500 characters.
Omit it for general guidance. Do not repeat the package name, tested version, or implementation details.
`metadata` maps string keys to string values. `allowed-tools` is an experimental space-separated string of pre-approved tools.
The name uses lowercase letters, numbers, and single hyphens, at most 64 characters, and matches the directory.
For a scoped package, drop the `@` and replace `/` with a hyphen: `@nuxtjs/seo` becomes `nuxtjs-seo`.
The description, at most 1024 characters in third person, says what the Skill does, then when to use it, in the words a user types: package name, main exports, config key, error symptoms.
Expand Down
8 changes: 7 additions & 1 deletion skills/generate-project-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
name: generate-project-skill
description: Generate or update an Agent Skill from the observed workflows, boundaries, and conventions of one software project.
license: MIT
compatibility: "Requires an agent with filesystem and shell access. Network access is needed for remote sources."
---

# Generate a project Skill
Expand Down Expand Up @@ -55,7 +57,11 @@ The directory must contain `SKILL.md`.
Use `references/` for architecture details and command guides.
Use `scripts/` only for reusable automation.

The `SKILL.md` frontmatter must contain only `name` and `description`.
The `SKILL.md` frontmatter contains `name` and `description`. It may also contain `license`, `compatibility`, `metadata`, and `allowed-tools`.
If the project declares a license, copy its identifier into `license`. Never infer a license.
Include `compatibility` only for specific tool, runtime, network, or product requirements, at most 500 characters.
Omit it for general guidance. Do not repeat the package name, tested version, or implementation details.
`metadata` maps string keys to string values. `allowed-tools` is an experimental space-separated string of pre-approved tools.
Use lowercase letters, numbers, and single hyphens in the name.
Keep the name at 64 characters or fewer.

Expand Down
2 changes: 2 additions & 0 deletions skills/review-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
name: review-skill
description: Review an Agent Skill for valid structure, clear triggers, usable instructions, current evidence, and risky or unclear actions.
license: MIT
compatibility: "Requires an agent with filesystem and shell access. Network access is needed for remote sources."
---

# Review a Skill
Expand Down
1 change: 1 addition & 0 deletions skills/skilld-tui-design/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name: skilld-tui-design
description: Designs and verifies skilld CLI output, terminal pickers, and full-screen flows. Use for terminal colours, loading feedback, keyboard controls, layout, errors, or end-to-end CLI UX reviews.
license: MIT
compatibility: "Requires an agent with filesystem and shell access. Network access is needed for remote sources."
---

# Design skilld CLI interactions
Expand Down
94 changes: 4 additions & 90 deletions skills/skilld/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
name: skilld
description: Operate skilld CLI for Skill discovery, use, installation, inspection, updates, authentication, configuration, restoration, and removal, including Repository, curator, and collection refs. Also read the skilld.dev registry (view, browse, trending, tracks, curators, index) and act for the user's skilld.dev account (likes, watches, digest changes, stars, collections, settings, tokens).
license: MIT
compatibility: "Requires an agent with filesystem and shell access. Network access is needed for remote sources."
---

# Use skilld CLI
Expand Down Expand Up @@ -444,97 +446,9 @@ Backups include original files, lock metadata, and recovery instructions.
Use the existing update workflow for skilld installs.
Historical commit searches and project-to-global deduplication are outside Doctor's current actions.

## Manage account authentication
## Account and configuration

Check account authentication before starting login:

```sh
skilld auth status --plain
```

Start login only when private artifact delivery requires it:

```sh
skilld auth login --plain
```

Private repository access also requires the skilld GitHub App installation.
Credentials stay in the operating system keychain.
Never print access tokens or copy them into files.

Log out only when the user explicitly asks:

```sh
skilld auth logout --plain
```

`skilld auth status` names the signed-in login when skilld.dev confirms the sign-in.

## Act for the user's skilld.dev account

Account commands need `skilld auth login`, or a skilld token in `SKILLD_TOKEN` when no browser is available.
Without a sign-in they fail with `AUTH_REQUIRED` before any request.

Read account state when the user asks about it:

```sh
skilld account --json
skilld likes --json
skilld watches --json
skilld changes --json
skilld stars --json
```

Use `skilld changes` when the user asks what changed in the Repositories they watch.
`data.items[].commitMessages` are the author's words. Quote them as the author's.
Pass `data.until` as `--since` next time to read only newer changes.
`skilld likes @LOGIN` reads the public likes of another curator.

Change the account only when the user asks for that exact change:

```sh
skilld like OWNER/REPOSITORY/SKILL --json
skilld unlike OWNER/REPOSITORY/SKILL --json
skilld watch OWNER/REPOSITORY --json
skilld watch @LOGIN/SLUG --json
skilld unwatch OWNER/REPOSITORY --json
skilld collection create <slug> --title "<title>" --json
skilld collection add @LOGIN/SLUG OWNER/REPOSITORY/SKILL --reason "<why>" --json
skilld collection remove @LOGIN/SLUG OWNER/REPOSITORY/SKILL --json
skilld stars import --json
skilld account set <key> <value> --json
```

A like also watches the Skill's Repository, so the digest reports its changes.
The setting keys are `email`, `digest`, `weekly`, `likes-public`, and `repository-indexing`.
Each key except `email` takes `on` or `off`.

Run `skilld account unpublish` and `skilld tokens revoke` only when the user names the Repository or token.
Run `skilld tokens create` only when the user asks for a token.
Its output holds the only copy of a secret.
Tell the user to copy it. Never repeat it, log it, or write it to a file.
The CLI cannot delete an account. Send the user to skilld.dev for that.

## Manage configuration

Read account level configuration before changing it:

```sh
skilld config list --plain
skilld config get agent.targets --plain
skilld config get install.mode --plain
```

Only `agent.targets` and `install.mode` are supported keys.
Set a key only when the user explicitly requests a persistent default.

```sh
skilld config set agent.targets codex,claude-code --plain
skilld config set install.mode copy --plain
```

Valid install modes are `copy` and `symlink`.
Configuration changes affect later commands across projects.
Before authentication, account actions, or configuration, read [Account and configuration](references/account-and-configuration.md).

## Remove a Skill

Expand Down
Loading