Add skill authoring lint workflow

This commit is contained in:
Martin Donadieu
2026-03-18 15:54:11 +01:00
parent eb3661c9f3
commit 823d7ebcd1
9 changed files with 283 additions and 3 deletions
+29
View File
@@ -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
+23 -1
View File
@@ -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
+7 -2
View File
@@ -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"
}
}
+94
View File
@@ -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);
});
+55
View File
@@ -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.
+27
View File
@@ -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
@@ -0,0 +1,6 @@
name: BrokenSkill
description: Skill docs.
# Broken Skill
This draft is intentionally poor.
@@ -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,
}));
+17
View File
@@ -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"
]
}