diff --git a/ai/commands/create-skill.md b/ai/commands/create-skill.md new file mode 100644 index 0000000..9fbf8e4 --- /dev/null +++ b/ai/commands/create-skill.md @@ -0,0 +1,8 @@ +## ๐Ÿ› ๏ธ Create Skill + +Use `ai/skills/create-skill/SKILL.md` to create a new agent skill following the AgentSkills.io specification. + +Constraints { + Begin by asking the user clarifying questions about the skill's purpose, inputs, outputs, and constraints. + Before beginning, read and respect the constraints in please.mdc. +} diff --git a/ai/commands/index.md b/ai/commands/index.md index f728833..1a8767a 100644 --- a/ai/commands/index.md +++ b/ai/commands/index.md @@ -10,6 +10,12 @@ This index provides an overview of the contents in this directory. *No description available* +### create-skill + +**File:** `create-skill.md` + +*No description available* + ### discover **File:** `discover.md` diff --git a/ai/index.md b/ai/index.md index 0419529..6654e0d 100644 --- a/ai/index.md +++ b/ai/index.md @@ -12,3 +12,7 @@ See [`commands/index.md`](./commands/index.md) for contents. See [`rules/index.md`](./rules/index.md) for contents. +### ๐Ÿ“ skills/ + +See [`skills/index.md`](./skills/index.md) for contents. + diff --git a/ai/rules/agent-orchestrator.mdc b/ai/rules/agent-orchestrator.mdc index 16703ec..58ec331 100644 --- a/ai/rules/agent-orchestrator.mdc +++ b/ai/rules/agent-orchestrator.mdc @@ -24,6 +24,7 @@ Agents { javascript-io-network-effects: when you need to make network requests or invoke side-effects, use this guide for saga pattern implementation ui: when building user interfaces and user experiences, use this guide for beautiful and friendly UI/UX design requirements: when writing functional requirements for a user story, use this guide for functional requirement specification + create-skill: when creating a new agent skill, use this guide for AgentSkills.io specification and SudoLang skill authoring } const taskPrompt = "# Guides\n\nRead each of the following guides for important context, and follow their instructions carefully: ${list guide file refs in markdown format}\n\n# User Prompt\n\n${prompt}" diff --git a/ai/rules/please.mdc b/ai/rules/please.mdc index 8b2b3db..62dfd49 100644 --- a/ai/rules/please.mdc +++ b/ai/rules/please.mdc @@ -43,6 +43,7 @@ Commands { ๐Ÿ”ฌ /review - conduct a thorough code review focusing on code quality, best practices, and adherence to project standards ๐Ÿงช /user-test - use user-testing.mdc to generate human and AI agent test scripts from user journeys ๐Ÿค– /run-test - execute AI agent test script in real browser with screenshots + ๐Ÿ› ๏ธ /create-skill - create a new agent skill using AgentSkills.io spec and SudoLang } Constraints { diff --git a/ai/skills/create-skill/SKILL.md b/ai/skills/create-skill/SKILL.md new file mode 100644 index 0000000..4394bf3 --- /dev/null +++ b/ai/skills/create-skill/SKILL.md @@ -0,0 +1,236 @@ +--- +name: create-skill +description: Create new agent skills following the AgentSkills.io specification. Use when the user wants to author, scaffold, or generate a new agent skill with proper structure, validation, and best practices. +metadata: + author: paralleldrive + version: "1.0" +--- +# Create Skill + +Create agent skills following the [AgentSkills.io](https://agentskills.io/specification) specification. Skills use **verb form** naming (e.g., `format-code`, `create-skill`). Agents use **noun form** (e.g., `code-formatter`, `skill-creator`). + +Read `ai/rules/sudolang/sudolang-syntax.mdc` for SudoLang syntax conventions. See `ai/rules/productmanager.mdc` for an example of well-written SudoLang (types, interfaces, composition). + +## Skill Structure + +A skill is a directory containing a `SKILL.md` file with optional supporting directories: + +``` +skill-name/ +โ”œโ”€โ”€ SKILL.md # Required: YAML frontmatter + markdown instructions +โ”œโ”€โ”€ scripts/ # Optional: executable code agents can run +โ”œโ”€โ”€ references/ # Optional: documentation loaded on demand +โ””โ”€โ”€ assets/ # Optional: templates, images, data files +``` + +## Types + +type SkillName = string(1-64, lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens, must match parent directory name) +type SkillDescription = string(1-1024, describes what skill does AND when to use it) + +## Interfaces + +Frontmatter { + name: SkillName // required + description: SkillDescription // required + license // optional + compatibility: string(1-500) // optional, environment requirements + metadata {} // optional, arbitrary key-value pairs (see AIDD Extensions below) + allowed-tools // optional, space-delimited tool list (experimental) +} + +### AIDD Extensions via `metadata` + +The AgentSkills.io spec uses progressive disclosure: only `name` and `description` (~100 tokens) are loaded at startup; the full SKILL.md body (< 5000 tokens recommended) loads only on activation; resources load on demand. + +The spec has no `alwaysApply` equivalent. AIDD uses `metadata.alwaysApply` to mark skills whose full instructions should be preloaded into agent context on project init. Use sparingly - every always-applied skill consumes context budget. + +```yaml +metadata: + alwaysApply: "true" # AIDD extension: preload full SKILL.md on project init +``` + +SizeMetrics { + frontmatterTokens: number // should be < 100 + bodyLines: number // should be < 160, must be < 500 + bodyTokens: number // should be < 5000 +} + +SkillPlan { + name: SkillName + purpose: SkillDescription + alwaysApply: boolean // should this skill preload on project init? + relatedSkills[] // existing skills found in discovery + bestPractices[] // findings from web research + proposedSections[] // planned SKILL.md structure + optionalDirs: ["scripts" | "references" | "assets"] + sizeEstimate: SizeMetrics +} + +## Process + +createSkill(userRequest) { + gatherRequirements + |> discoverRelatedSkills + |> researchBestPractices + |> nameSkill + |> buildPlan + |> presentPlan + |> awaitApproval + |> draftSkillMd + |> writeSkill + |> validate + |> reportMetrics +} + +gatherRequirements(userRequest) { + ask clarifying questions about: + - purpose: what problem does this skill solve? + - inputs and outputs: what does the skill consume and produce? + - constraints: any technical limitations or requirements? + - alwaysApply: should this skill be preloaded into agent context on every session? + hint: recommend "yes" only if the skill applies to nearly every task (e.g., coding standards, security checks) + hint: recommend "no" for task-specific skills that activate on demand (e.g., pdf-processing, deploy-app) +} + +discoverRelatedSkills(skillTopic) { + searchPaths = ["$projectRoot/ai/", "$projectRoot/aidd-custom/"] + scan for SKILL.md, .mdc, and .md files + read frontmatter descriptions + identify overlapping or complementary skills + report: related skills, how to leverage them, overlap to avoid +} + +researchBestPractices(skillTopic) { + use web search to find best practices for $skillTopic + summarize key findings relevant to skill authoring +} + +nameSkill(topic) { + use verb form (e.g., `format-code`, `generate-report`) + validate against SkillName type constraints +} + +buildPlan() => SkillPlan + +presentPlan(plan: SkillPlan) { + show the user the full SkillPlan + ask if any changes are required + await explicit user approval +} + +draftSkillMd(plan: SkillPlan) { + write Frontmatter with name + description (required), optional fields as needed + if (plan.alwaysApply) add `metadata.alwaysApply: "true"` + write body with required documentation sections (see Documentation Requirements below) +} + +writeSkill(skillMd) { + write to `$projectRoot/aidd-custom/${skillName}/SKILL.md` + create optional directories (scripts/, references/, assets/) if planned +} + +## Validation + +After creating the skill, validate using the unit-tested validator at `scripts/validate-skill.js`: + +```bash +# If skills-ref is available: +skills-ref validate ./path-to-skill-directory + +# Then run the symbolic size/name validator: +node ai/skills/create-skill/scripts/validate-skill.js ./path-to-skill-directory +``` + +The validator exports: `parseSkillMd`, `validateName`, `calculateMetrics`, `checkThresholds`. + +## Documentation Requirements + +Every generated skill MUST include these documentation sections in the SKILL.md body: + +RequiredSections { + "# Title" // skill name as heading + "## When to use" // clear activation criteria - when should an agent use this skill? + "## Steps" | "## Process" // step-by-step instructions for executing the skill + "## Examples" // concrete input/output examples demonstrating usage + "## Edge cases" // known limitations, error handling, boundary conditions +} + +The `description` field is the skill's elevator pitch - it must be good enough for an agent to decide whether to activate the skill based on description alone. Write it as if it were the only thing an agent reads before deciding. + +## Constraints + +Constraints { + // Structure + NEVER create a standalone .mdc or .md file as a skill + The output MUST be `$projectRoot/aidd-custom/${skillName}/SKILL.md` + The file MUST be named exactly `SKILL.md` (uppercase) + + // Frontmatter + Frontmatter MUST include `name` and `description` as required fields + NEVER use non-spec keys as top-level frontmatter fields + Use `metadata` for AIDD extensions (e.g., `metadata.alwaysApply: "true"`) + The `name` field MUST satisfy the SkillName type + The `description` field MUST satisfy the SkillDescription type + + // Size + SKILL.md body SHOULD stay under 160 lines and MUST stay under 500 lines + SKILL.md body SHOULD stay under 5000 tokens + If content exceeds limits, split into references/ directory files + + // Documentation + SKILL.md body MUST include all RequiredSections + The description field MUST be specific enough for agent activation decisions + Include concrete examples - not just abstract instructions + + // Validation + ALWAYS run `scripts/validate-skill.js` after creating the skill + ALWAYS run `skills-ref validate` if available + ALWAYS report size metrics and any warnings to the user +} + +## Example SKILL.md + +```markdown +--- +name: format-code +description: Format source code files according to project style guides and conventions. Use when code needs formatting, linting fixes, style consistency checks, or when a user mentions "format", "lint", or "prettier". +metadata: + author: my-org + version: "1.0" + alwaysApply: "true" +--- + +# Format Code + +Format source code according to project conventions and style guides. + +## When to use + +Use this skill when: +- Code needs formatting or linting fixes +- A new file is created and needs style consistency +- The user asks for style or formatting help + +## Steps + +1. Detect the project's formatter configuration (prettier, biome, eslint, etc.) +2. Identify files that need formatting +3. Apply formatting rules +4. Report changes made + +## Examples + +Given a project with `.prettierrc`, run: `npx prettier --write "src/**/*.ts"` +Given a project with `biome.json`, run: `npx @biomejs/biome format --write` + +## Edge cases + +- If no formatter config exists, ask the user which style to use +- For mixed-language projects, apply per-language formatters +- If formatter conflicts with linter, prioritize linter configuration +``` + +Commands { + /create-skill - create a new agent skill following the AgentSkills.io specification +} diff --git a/ai/skills/create-skill/index.md b/ai/skills/create-skill/index.md new file mode 100644 index 0000000..dba0dfd --- /dev/null +++ b/ai/skills/create-skill/index.md @@ -0,0 +1,18 @@ +# create-skill + +This index provides an overview of the contents in this directory. + +## Subdirectories + +### ๐Ÿ“ scripts/ + +See [`scripts/index.md`](./scripts/index.md) for contents. + +## Files + +### Create Skill + +**File:** `SKILL.md` + +Create new agent skills following the AgentSkills.io specification. Use when the user wants to author, scaffold, or generate a new agent skill with proper structure, validation, and best practices. + diff --git a/ai/skills/create-skill/scripts/index.md b/ai/skills/create-skill/scripts/index.md new file mode 100644 index 0000000..36ce914 --- /dev/null +++ b/ai/skills/create-skill/scripts/index.md @@ -0,0 +1,5 @@ +# scripts + +This index provides an overview of the contents in this directory. + +*This directory is empty.* diff --git a/ai/skills/create-skill/scripts/validate-skill.js b/ai/skills/create-skill/scripts/validate-skill.js new file mode 100644 index 0000000..7406ae6 --- /dev/null +++ b/ai/skills/create-skill/scripts/validate-skill.js @@ -0,0 +1,47 @@ +/** + * Validate an AgentSkills.io SKILL.md file. + * + * Usage: node validate-skill.js ./path-to-skill-directory + */ + +export const parseSkillMd = (content) => { + const match = content.match(/^---\n([\s\S]*?)\n---/); + if (!match) return { body: content, frontmatter: "" }; + return { + body: content.slice(match[0].length).trim(), + frontmatter: match[1], + }; +}; + +export const validateName = (name, dirName) => { + const errors = []; + if (name.length < 1 || name.length > 64) + errors.push("Name must be 1-64 characters"); + if (/[^a-z0-9-]/.test(name)) + errors.push("Name must be lowercase alphanumeric + hyphens only"); + if (/^-|-$/.test(name)) errors.push("Name must not start or end with hyphen"); + if (/--/.test(name)) errors.push("Name must not contain consecutive hyphens"); + if (name !== dirName) errors.push("Name must match directory name"); + return errors; +}; + +export const calculateMetrics = (frontmatter, body) => ({ + bodyLines: body.split("\n").length, + bodyTokens: Math.ceil(body.length / 4), + frontmatterTokens: Math.ceil(frontmatter.length / 4), +}); + +export const checkThresholds = (metrics) => { + const warnings = []; + if (metrics.frontmatterTokens >= 100) + warnings.push("Frontmatter exceeds 100 token guideline"); + if (metrics.bodyLines >= 160) + warnings.push("Body exceeds 160 line guideline"); + if (metrics.bodyLines >= 500) + warnings.push( + "Body exceeds 500 line spec limit - split into reference files", + ); + if (metrics.bodyTokens >= 5000) + warnings.push("Body exceeds 5000 token spec guideline"); + return warnings; +}; diff --git a/ai/skills/create-skill/scripts/validate-skill.test.js b/ai/skills/create-skill/scripts/validate-skill.test.js new file mode 100644 index 0000000..af90d4e --- /dev/null +++ b/ai/skills/create-skill/scripts/validate-skill.test.js @@ -0,0 +1,242 @@ +import { assert } from "riteway/vitest"; +import { describe, test } from "vitest"; + +import { + calculateMetrics, + checkThresholds, + parseSkillMd, + validateName, +} from "./validate-skill.js"; + +describe("parseSkillMd", () => { + test("valid frontmatter", () => { + const content = `--- +name: my-skill +description: A test skill. +--- +# My Skill + +Body content here.`; + + const result = parseSkillMd(content); + + assert({ + given: "SKILL.md content with valid frontmatter", + should: "return parsed frontmatter string", + actual: result.frontmatter.includes("name: my-skill"), + expected: true, + }); + + assert({ + given: "SKILL.md content with valid frontmatter", + should: "return body without frontmatter", + actual: result.body.startsWith("# My Skill"), + expected: true, + }); + }); + + test("no frontmatter", () => { + const content = "# Just a body\n\nNo frontmatter here."; + const result = parseSkillMd(content); + + assert({ + given: "content without frontmatter", + should: "return empty frontmatter", + actual: result.frontmatter, + expected: "", + }); + + assert({ + given: "content without frontmatter", + should: "return full content as body", + actual: result.body, + expected: content, + }); + }); +}); + +describe("validateName", () => { + test("valid names", () => { + assert({ + given: "a valid lowercase hyphenated name", + should: "return no errors", + actual: validateName("format-code", "format-code"), + expected: [], + }); + + assert({ + given: "a single-word name", + should: "return no errors", + actual: validateName("lint", "lint"), + expected: [], + }); + }); + + test("uppercase letters", () => { + const errors = validateName("Format-Code", "Format-Code"); + + assert({ + given: "a name with uppercase letters", + should: "return a validation error", + actual: errors.length > 0, + expected: true, + }); + }); + + test("leading hyphen", () => { + const errors = validateName("-my-skill", "-my-skill"); + + assert({ + given: "a name starting with a hyphen", + should: "return a validation error", + actual: errors.length > 0, + expected: true, + }); + }); + + test("trailing hyphen", () => { + const errors = validateName("my-skill-", "my-skill-"); + + assert({ + given: "a name ending with a hyphen", + should: "return a validation error", + actual: errors.length > 0, + expected: true, + }); + }); + + test("consecutive hyphens", () => { + const errors = validateName("my--skill", "my--skill"); + + assert({ + given: "a name with consecutive hyphens", + should: "return a validation error", + actual: errors.length > 0, + expected: true, + }); + }); + + test("too long", () => { + const longName = "a".repeat(65); + const errors = validateName(longName, longName); + + assert({ + given: "a name longer than 64 characters", + should: "return a validation error", + actual: errors.length > 0, + expected: true, + }); + }); + + test("empty name", () => { + const errors = validateName("", "some-dir"); + + assert({ + given: "an empty name", + should: "return a validation error", + actual: errors.length > 0, + expected: true, + }); + }); + + test("directory mismatch", () => { + const errors = validateName("my-skill", "other-dir"); + + assert({ + given: "a name that does not match the directory name", + should: "return a validation error", + actual: errors.length > 0, + expected: true, + }); + }); +}); + +describe("calculateMetrics", () => { + test("calculates correct metrics", () => { + const frontmatter = "name: test\ndescription: A test."; + const body = "# Title\n\nLine 2\nLine 3"; + const result = calculateMetrics(frontmatter, body); + + assert({ + given: "frontmatter and body text", + should: "estimate frontmatter tokens as ceil(length / 4)", + actual: result.frontmatterTokens, + expected: Math.ceil(frontmatter.length / 4), + }); + + assert({ + given: "a body with 4 lines", + should: "count 4 body lines", + actual: result.bodyLines, + expected: 4, + }); + + assert({ + given: "body text", + should: "estimate body tokens as ceil(length / 4)", + actual: result.bodyTokens, + expected: Math.ceil(body.length / 4), + }); + }); +}); + +describe("checkThresholds", () => { + test("all within limits", () => { + const metrics = { frontmatterTokens: 50, bodyLines: 100, bodyTokens: 3000 }; + + assert({ + given: "metrics within all thresholds", + should: "return no warnings", + actual: checkThresholds(metrics), + expected: [], + }); + }); + + test("frontmatter too large", () => { + const metrics = { frontmatterTokens: 100, bodyLines: 50, bodyTokens: 1000 }; + const warnings = checkThresholds(metrics); + + assert({ + given: "frontmatter at 100 tokens", + should: "return a frontmatter warning", + actual: warnings.some((w) => w.includes("Frontmatter")), + expected: true, + }); + }); + + test("body exceeds 160 lines", () => { + const metrics = { frontmatterTokens: 10, bodyLines: 160, bodyTokens: 1000 }; + const warnings = checkThresholds(metrics); + + assert({ + given: "body at 160 lines", + should: "return a 160-line warning", + actual: warnings.some((w) => w.includes("160")), + expected: true, + }); + }); + + test("body exceeds 500 lines", () => { + const metrics = { frontmatterTokens: 10, bodyLines: 500, bodyTokens: 1000 }; + const warnings = checkThresholds(metrics); + + assert({ + given: "body at 500 lines", + should: "return a 500-line spec limit warning", + actual: warnings.some((w) => w.includes("500")), + expected: true, + }); + }); + + test("body exceeds 5000 tokens", () => { + const metrics = { frontmatterTokens: 10, bodyLines: 50, bodyTokens: 5000 }; + const warnings = checkThresholds(metrics); + + assert({ + given: "body at 5000 tokens", + should: "return a token warning", + actual: warnings.some((w) => w.includes("5000")), + expected: true, + }); + }); +}); diff --git a/ai/skills/index.md b/ai/skills/index.md new file mode 100644 index 0000000..fb328c5 --- /dev/null +++ b/ai/skills/index.md @@ -0,0 +1,10 @@ +# skills + +This index provides an overview of the contents in this directory. + +## Subdirectories + +### ๐Ÿ“ create-skill/ + +See [`create-skill/index.md`](./create-skill/index.md) for contents. +