diff --git a/.github/workflows/lint-skills.yml b/.github/workflows/lint-skills.yml new file mode 100644 index 0000000..aeeb3fc --- /dev/null +++ b/.github/workflows/lint-skills.yml @@ -0,0 +1,29 @@ +name: Lint Skills + +on: + push: + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Lint skills + env: + ENABLE_SKILLGRADE: "1" + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + run: bun run lint-skills diff --git a/README.md b/README.md index f0289b0..d3ad28b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ > Formerly `@capgo/capacitor-skills` (and `Cap-go/capacitor-skills`). Links and redirects should continue to work. -A collection of **28 skills** for AI coding agents working with Capacitor, the cross-platform native runtime. Skills are packaged instructions that extend agent capabilities for mobile development. +A collection of **29 skills** for AI coding agents working with Capacitor, the cross-platform native runtime. Skills are packaged instructions that extend agent capabilities for mobile development. ## Compatibility @@ -93,6 +93,12 @@ bunx skills add Cap-go/capgo-skills | [capacitor-plugin-spm-support](./skills/capacitor-plugin-spm-support) | Add Swift Package Manager support to a plugin | | [cocoapods-to-spm](./skills/cocoapods-to-spm) | Migrate to Swift Package Manager | +### Authoring + +| Skill | Description | +|-------|-------------| +| [skill-creator](./skills/skill-creator) | Create and validate new skills with progressive disclosure | + ### Upgrades | Skill | Description | @@ -130,6 +136,10 @@ Skills activate automatically when agents detect relevant tasks: - "Submit to Play Store" → capacitor-app-store - "Add SPM support to a plugin" → capacitor-plugin-spm-support +### Authoring +- "Create a new skill" → skill-creator +- "Validate a skill" → skill-creator + ### Upgrades - "Upgrade a Capacitor app" → capacitor-app-upgrades - "Upgrade a Capacitor plugin" → capacitor-plugin-upgrades @@ -210,6 +220,18 @@ Add new skills by creating a folder in `skills/` with: - `SKILL.md` - Instructions for agents - `metadata.json` - Skill metadata +Validate the pack locally with: + +```bash +bun run lint-skills +``` + +Run the skillgrade-backed eval for the skill authoring workflow with an API key: + +```bash +ENABLE_SKILLGRADE=1 bun run lint-skills-skillgrade +``` + ## License MIT diff --git a/package.json b/package.json index e40277b..4509456 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@capgo/capgo-skills", "version": "1.1.0", - "description": "28 agent skills for Capacitor mobile development", + "description": "29 agent skills for Capacitor mobile development", "keywords": [ "capacitor", "capacitor-skills", @@ -50,6 +50,11 @@ "cocoapods-to-spm", "cordova-to-capacitor", "framework-to-capacitor", + "skill-creator", "ionic-enterprise-sdk-migration" - ] + ], + "scripts": { + "lint-skills": "bun scripts/lint-skills.mjs", + "lint-skills-skillgrade": "bunx skillgrade --ci --provider=local --smoke" + } } diff --git a/scripts/lint-skills.mjs b/scripts/lint-skills.mjs new file mode 100644 index 0000000..f633e19 --- /dev/null +++ b/scripts/lint-skills.mjs @@ -0,0 +1,94 @@ +import { readFile, readdir } from 'node:fs/promises'; +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; + +const root = process.cwd(); +const skillsDir = path.join(root, 'skills'); + +function parseFrontmatter(text) { + const match = text.match(/^---\n([\s\S]*?)\n---/); + if (!match) return null; + + const frontmatter = {}; + for (const line of match[1].split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const colon = trimmed.indexOf(':'); + if (colon === -1) continue; + const key = trimmed.slice(0, colon).trim(); + const value = trimmed.slice(colon + 1).trim().replace(/^["']|["']$/g, ''); + frontmatter[key] = value; + } + + return frontmatter; +} + +async function main() { + const entries = await readdir(skillsDir, { withFileTypes: true }); + const skillDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort(); + const errors = []; + + for (const skillName of skillDirs) { + const skillPath = path.join(skillsDir, skillName, 'SKILL.md'); + let content; + try { + content = await readFile(skillPath, 'utf8'); + } catch { + errors.push(`${skillName}: missing SKILL.md`); + continue; + } + + const frontmatter = parseFrontmatter(content); + if (!frontmatter) { + errors.push(`${skillName}: missing YAML frontmatter`); + continue; + } + + if (frontmatter.name !== skillName) { + errors.push(`${skillName}: name "${frontmatter.name ?? ''}" does not match folder name`); + } + + if (!frontmatter.description) { + errors.push(`${skillName}: missing description`); + } else if (frontmatter.description.length > 1024) { + errors.push(`${skillName}: description exceeds 1024 characters`); + } + + if (!content.includes('## When to Use') && !content.includes('## When to Use This Skill')) { + errors.push(`${skillName}: missing usage guidance`); + } + } + + if (errors.length > 0) { + console.error('Skill lint failed:'); + for (const error of errors) { + console.error(`- ${error}`); + } + process.exit(1); + } + + const shouldRunSkillgrade = process.env.ENABLE_SKILLGRADE === '1'; + const hasApiKey = Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY); + if (shouldRunSkillgrade && hasApiKey) { + const result = spawnSync('bunx', ['skillgrade', '--ci', '--provider=local', '--smoke'], { + cwd: path.join(skillsDir, 'skill-creator'), + encoding: 'utf8', + stdio: 'pipe', + }); + + if (result.status !== 0) { + process.stderr.write(result.stdout || ''); + process.stderr.write(result.stderr || ''); + process.exit(result.status ?? 1); + } + } else { + console.log('Skipping skillgrade eval: set ENABLE_SKILLGRADE=1 with an API key to run it.'); + } + + console.log(`Validated ${skillDirs.length} skills.`); +} + +main().catch((error) => { + console.error(error?.stack || String(error)); + process.exit(1); +}); diff --git a/skills/skill-creator/SKILL.md b/skills/skill-creator/SKILL.md new file mode 100644 index 0000000..a590b93 --- /dev/null +++ b/skills/skill-creator/SKILL.md @@ -0,0 +1,55 @@ +--- +name: skill-creator +description: Guides the agent through authoring and validating agent skills. Use when creating new skill directories, tightening skill metadata, extracting supporting references, or preparing skillgrade evals. Do not use for general app documentation, generic README editing, or non-agentic library code. +--- + +# Skill Authoring Procedure + +Create professional-grade skills with lean context, deterministic structure, and validation. + +## When to Use This Skill + +- User wants to create a new skill directory +- User wants to improve a skill's discoverability or metadata +- User wants to split large instructions into references or scripts +- User wants to add or update skillgrade validation + +## Procedures + +### Step 1: Validate the Skill Metadata + +Check that the frontmatter uses a unique lowercase name, a specific description, and clear negative triggers. + +Keep the description short enough to fit within the agent router's metadata budget. + +### Step 2: Keep the Main Skill Lean + +Write the main `SKILL.md` as a high-level workflow. + +Move dense rules, large schemas, and reusable templates into `references/` or `assets/`. + +Use `scripts/` only for fragile or repetitive logic that should not be re-authored by the agent. + +### Step 3: Use Progressive Disclosure + +Command the agent to read supporting files only when the current step needs them. + +Prefer one-level-deep support files with explicit relative paths. + +### Step 4: Add Validation + +Create a `skillgrade` eval when the skill needs regression testing. + +Use a deterministic grader for structural checks and an LLM rubric only when qualitative judgment is necessary. + +### Step 5: Review for Hallucination Gaps + +Inspect the skill for any step where the agent is forced to guess. + +Replace ambiguous prose with concrete commands, file names, or output expectations. + +## Error Handling + +- If a skill cannot be validated, reduce scope until the missing behavior becomes testable. +- If the description is too broad, tighten the trigger text before adding more instructions. +- If the supporting material grows too large, extract it into a separate file and point the agent to it explicitly. diff --git a/skills/skill-creator/eval.yaml b/skills/skill-creator/eval.yaml new file mode 100644 index 0000000..c08aea6 --- /dev/null +++ b/skills/skill-creator/eval.yaml @@ -0,0 +1,27 @@ +# Skill Authoring Example + +version: "1" + +defaults: + agent: codex + provider: local + trials: 3 + timeout: 300 + threshold: 0.8 + +tasks: + - name: fix-skill-draft + instruction: | + Rewrite `draft/SKILL.md` so it follows the skill authoring standard. + + Keep the file lean, use YAML frontmatter, include a clear "When to Use This Skill" section, and add an "Error Handling" section. + Preserve the overall purpose of the draft, but make the metadata and structure production-ready. + + workspace: + - src: fixtures/broken-skill/SKILL.md + dest: draft/SKILL.md + + graders: + - type: deterministic + run: bun graders/check-skill.js + weight: 1 diff --git a/skills/skill-creator/fixtures/broken-skill/SKILL.md b/skills/skill-creator/fixtures/broken-skill/SKILL.md new file mode 100644 index 0000000..6ef8e35 --- /dev/null +++ b/skills/skill-creator/fixtures/broken-skill/SKILL.md @@ -0,0 +1,6 @@ +name: BrokenSkill +description: Skill docs. + +# Broken Skill + +This draft is intentionally poor. diff --git a/skills/skill-creator/graders/check-skill.js b/skills/skill-creator/graders/check-skill.js new file mode 100644 index 0000000..9db8e8c --- /dev/null +++ b/skills/skill-creator/graders/check-skill.js @@ -0,0 +1,25 @@ +import { readFileSync } from 'node:fs'; + +const skill = readFileSync('draft/SKILL.md', 'utf8'); + +const checks = []; +let passed = 0; + +function addCheck(name, condition, message) { + checks.push({ name, passed: condition, message: condition ? 'OK' : message }); + if (condition) passed += 1; +} + +addCheck('frontmatter', /^---\n[\s\S]*?\n---/m.test(skill), 'Missing YAML frontmatter'); +addCheck('name', /name:\s*skill-creator\b/.test(skill), 'name must be skill-creator'); +addCheck('description', /description:\s*.+/.test(skill), 'description missing'); +addCheck('usage', /## When to Use This Skill/.test(skill), 'Missing usage section'); +addCheck('error-handling', /## Error Handling/.test(skill), 'Missing error handling section'); +addCheck('no-readme', !/README\.md/.test(skill), 'Should not mention README.md'); + +const score = (passed / checks.length).toFixed(2); +console.log(JSON.stringify({ + score: Number(score), + details: `${passed}/${checks.length} checks passed`, + checks, +})); diff --git a/skills/skill-creator/metadata.json b/skills/skill-creator/metadata.json new file mode 100644 index 0000000..35a4a51 --- /dev/null +++ b/skills/skill-creator/metadata.json @@ -0,0 +1,17 @@ +{ + "version": "1.0.0", + "organization": "Capgo", + "date": "March 2026", + "abstract": "Guide for authoring high-quality agent skills with lean instructions, progressive disclosure, and skillgrade validation.", + "triggers": [ + "create a skill", + "skill authoring", + "agent skill best practices", + "validate a skill", + "skillgrade" + ], + "references": [ + "https://agentskills.io", + "https://github.com/mgechev/skillgrade" + ] +}