diff --git a/packages/harness/README.md b/packages/harness/README.md index ddb1598a..0562398e 100644 --- a/packages/harness/README.md +++ b/packages/harness/README.md @@ -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. diff --git a/packages/harness/src/internal/output/validate.ts b/packages/harness/src/internal/output/validate.ts index 76b1c77f..005b7a2e 100644 --- a/packages/harness/src/internal/output/validate.ts +++ b/packages/harness/src/internal/output/validate.ts @@ -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 @@ -29,8 +29,9 @@ export type GenerationContract = | { readonly _tag: 'PackageSkill' } | { readonly _tag: 'ProjectSkill', readonly projectPaths: ReadonlyArray } -const invalid = (issues: ReadonlyArray): Result => - err({ _tag: 'InvalidSkill', message: 'Skill output failed deterministic checks.', issues }) +function invalid(issues: ReadonlyArray): Result { + return err({ _tag: 'InvalidSkill', message: 'Skill output failed deterministic checks.', issues }) +} function decodeText(content: Uint8Array): string | null { try { @@ -82,11 +83,7 @@ function structureIssues(markdown: string, files: ReadonlyArray, return issues } -export const validateGeneratedSkill = ( - name: string, - files: ReadonlyArray, - contract: GenerationContract, -): Result => { +export function validateGeneratedSkill(name: string, files: ReadonlyArray, contract: GenerationContract): Result { const issues: string[] = [] const skillFiles = files.filter(file => file.path === 'SKILL.md') if (skillFiles.length !== 1) @@ -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.') @@ -141,9 +149,7 @@ export const validateGeneratedSkill = ( const findingLevels = new Set(['error', 'warning', 'note']) -export const validateSkillReview = ( - files: ReadonlyArray, -): Result => { +export function validateSkillReview(files: ReadonlyArray): Result { if (files.length !== 1 || files[0]?.path !== 'review.json') return invalid(['Review output must contain only review.json.']) diff --git a/packages/harness/test/e2e/output-checks.test.ts b/packages/harness/test/e2e/output-checks.test.ts index 58e0e202..94dfc455 100644 --- a/packages/harness/test/e2e/output-checks.test.ts +++ b/packages/harness/test/e2e/output-checks.test.ts @@ -82,7 +82,7 @@ async function runTurns(outputs: ReadonlyArray) { } 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 () => { @@ -90,13 +90,13 @@ describe('output check feedback', () => { 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) }) }) @@ -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` }) @@ -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.', ]) }) diff --git a/skills/generate-package-skill/SKILL.md b/skills/generate-package-skill/SKILL.md index fee17ccd..2e55502f 100644 --- a/skills/generate-package-skill/SKILL.md +++ b/skills/generate-package-skill/SKILL.md @@ -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 @@ -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. diff --git a/skills/generate-project-skill/SKILL.md b/skills/generate-project-skill/SKILL.md index 8f6bac7c..c8d271db 100644 --- a/skills/generate-project-skill/SKILL.md +++ b/skills/generate-project-skill/SKILL.md @@ -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 @@ -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. diff --git a/skills/review-skill/SKILL.md b/skills/review-skill/SKILL.md index c41b5457..409fcb65 100644 --- a/skills/review-skill/SKILL.md +++ b/skills/review-skill/SKILL.md @@ -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 diff --git a/skills/skilld-tui-design/SKILL.md b/skills/skilld-tui-design/SKILL.md index 056fa95d..8150405d 100644 --- a/skills/skilld-tui-design/SKILL.md +++ b/skills/skilld-tui-design/SKILL.md @@ -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 diff --git a/skills/skilld/SKILL.md b/skills/skilld/SKILL.md index 3e89a962..618318ad 100644 --- a/skills/skilld/SKILL.md +++ b/skills/skilld/SKILL.md @@ -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 @@ -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 --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 diff --git a/skills/skilld/references/account-and-configuration.md b/skills/skilld/references/account-and-configuration.md new file mode 100644 index 00000000..b637951c --- /dev/null +++ b/skills/skilld/references/account-and-configuration.md @@ -0,0 +1,94 @@ +# Account and configuration + +## Manage account authentication + +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. + diff --git a/skills/update-package-skill/SKILL.md b/skills/update-package-skill/SKILL.md index 6e0efcb6..8a37a820 100644 --- a/skills/update-package-skill/SKILL.md +++ b/skills/update-package-skill/SKILL.md @@ -1,6 +1,8 @@ --- name: update-package-skill description: Updates an existing package Skill for a new release of its npm or local package by testing only what changed since the version the Skill names. Use when a maintainer asks to refresh a package Skill after a release, or when a SKILL.md names an older tested version than the package. +license: MIT +compatibility: "Requires an agent with filesystem and shell access. Network access is needed for remote sources." --- # Update a package Skill @@ -49,7 +51,10 @@ If no change is relevant, leave the copy byte-identical and stop. Report "no cha Keep the format rules: -- The frontmatter contains only `name` and `description`. The name matches the directory. +- The frontmatter contains `name` and `description`, with optional `license`, `compatibility`, `metadata`, and `allowed-tools`. The name matches the directory. +- Preserve the source license. Keep `compatibility` only for specific environment requirements, at most 500 characters. +- Remove generic compatibility filler. Keep package version coverage in the body. +- `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` stays under 500 lines. Write at most eight reference files, each linked from `SKILL.md`.