From 7ebb6e906db8cabf7557778b6f2689442dce41e4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 10 Apr 2026 09:33:10 +0000 Subject: [PATCH 1/4] feat(skills): add /aidd-upskill Split from PR #94. One skill per PR per project standards. Adds the /aidd-upskill skill for creating and reviewing AIDD agent skills, including: - Skill creation and review pipelines (SKILL.md, references, scripts) - Command definition (ai/commands/aidd-upskill.md) - AI eval tests (caveman, dedup, function tests) - validate-skill CLI and its unit tests - Epic documentation (tasks/aidd-upskill-epic.md) - Upskill entries in aidd-agent-orchestrator, aidd-please, index files - .gitignore and package.json entries for the validator binary and eval script --- .gitignore | 3 + ai-evals/aidd-upskill/caveman-test.sudo | 18 + ai-evals/aidd-upskill/dedup-test.sudo | 17 + .../aidd-upskill/fixtures/dedup-reference.md | 10 + ai-evals/aidd-upskill/fixtures/dedup-skill.md | 24 + .../fixtures/sample-format-skill.md | 30 + ai-evals/aidd-upskill/function-test.sudo | 21 + ai/commands/aidd-upskill.md | 13 + ai/commands/index.md | 6 + ai/skills/aidd-agent-orchestrator/SKILL.md | 1 + ai/skills/aidd-please/SKILL.md | 1 + ai/skills/aidd-upskill/README.md | 34 + ai/skills/aidd-upskill/SKILL.md | 134 +++ ai/skills/aidd-upskill/index.md | 22 + ai/skills/aidd-upskill/references/index.md | 18 + ai/skills/aidd-upskill/references/process.md | 93 ++ ai/skills/aidd-upskill/references/types.md | 75 ++ ai/skills/aidd-upskill/scripts/index.md | 8 + .../aidd-upskill/scripts/validate-skill.js | 170 ++++ .../scripts/validate-skill.test.js | 867 ++++++++++++++++++ ai/skills/index.md | 1 + package.json | 2 + tasks/aidd-upskill-epic.md | 40 + 23 files changed, 1608 insertions(+) create mode 100644 ai-evals/aidd-upskill/caveman-test.sudo create mode 100644 ai-evals/aidd-upskill/dedup-test.sudo create mode 100644 ai-evals/aidd-upskill/fixtures/dedup-reference.md create mode 100644 ai-evals/aidd-upskill/fixtures/dedup-skill.md create mode 100644 ai-evals/aidd-upskill/fixtures/sample-format-skill.md create mode 100644 ai-evals/aidd-upskill/function-test.sudo create mode 100644 ai/commands/aidd-upskill.md create mode 100644 ai/skills/aidd-upskill/README.md create mode 100644 ai/skills/aidd-upskill/SKILL.md create mode 100644 ai/skills/aidd-upskill/index.md create mode 100644 ai/skills/aidd-upskill/references/index.md create mode 100644 ai/skills/aidd-upskill/references/process.md create mode 100644 ai/skills/aidd-upskill/references/types.md create mode 100644 ai/skills/aidd-upskill/scripts/index.md create mode 100644 ai/skills/aidd-upskill/scripts/validate-skill.js create mode 100644 ai/skills/aidd-upskill/scripts/validate-skill.test.js create mode 100644 tasks/aidd-upskill-epic.md diff --git a/.gitignore b/.gitignore index 16657fc3..ab0ec9e4 100644 --- a/.gitignore +++ b/.gitignore @@ -149,3 +149,6 @@ core.* # Local AIDD database .aidd/ +# Compiled validate-skill binary (built via npm run build:validate-skill) +ai/skills/aidd-upskill/scripts/validate-skill + diff --git a/ai-evals/aidd-upskill/caveman-test.sudo b/ai-evals/aidd-upskill/caveman-test.sudo new file mode 100644 index 00000000..7b28d14b --- /dev/null +++ b/ai-evals/aidd-upskill/caveman-test.sudo @@ -0,0 +1,18 @@ +# caveman-test.sudo + +import 'ai/skills/aidd-upskill/SKILL.md' + +userPrompt = """ +Use caveman() to evaluate this skill design decision: + +A skill is being designed to both fetch external API data AND format the response +into a markdown report. Should these two responsibilities live in a single skill +command, or be split into two separate commands? +""" + +- Given the caveman() function is invoked, should produce a ๐ŸŽฏ restate stage that restates the problem in plain terms (e.g. one command vs two commands, fetch + format responsibilities) +- Given the caveman() function is invoked, should produce a ๐Ÿ’ก ideate stage that generates at least two distinct design options (e.g. single combined command, split into fetch and format commands) +- Given the caveman() function is invoked, should produce a ๐Ÿชž reflect stage that critically identifies a flaw in the ideation โ€” such as noting that mixing a side effect (fetch) with a thinking stage (format) violates the "never mix thinking and effects" constraint from the skill +- Given the caveman() function is invoked, should produce a ๐Ÿ”ญ expand stage that explores orthogonal considerations not yet covered (e.g. testability, composability, progressive disclosure) +- Given the caveman() function is invoked, should produce a โš–๏ธ score stage that ranks or evaluates the options against explicit criteria +- Given the caveman() function is invoked, should produce a ๐Ÿ’ฌ respond stage with a concrete recommendation diff --git a/ai-evals/aidd-upskill/dedup-test.sudo b/ai-evals/aidd-upskill/dedup-test.sudo new file mode 100644 index 00000000..efbebc5d --- /dev/null +++ b/ai-evals/aidd-upskill/dedup-test.sudo @@ -0,0 +1,17 @@ +# dedup-test.sudo + +import 'ai/skills/aidd-upskill/SKILL.md' + +userPrompt = """ +Run deduplicateWithCaveman() on the following two files: + +SKILL.md body: ai-evals/aidd-upskill/fixtures/dedup-skill.md +Reference file: ai-evals/aidd-upskill/fixtures/dedup-reference.md + +Identify every piece of information that appears in both files and determine +where the single source of truth should live. +""" + +- Given the SkillName constraints (lowercase alphanumeric, 1-64 chars, aidd- prefix, match parent dir) appear in both dedup-skill.md and dedup-reference.md, should identify the name constraint rules as duplicated information present in both files +- Given the name constraints are duplicated, should identify the reference file (dedup-reference.md) as the canonical location because it is titled "Name Constraints" and explicitly notes it is the canonical definition +- Given the name constraints are in the reference file, should recommend removing the duplicated name constraint prose from the SKILL.md body and relying solely on the import of the reference file diff --git a/ai-evals/aidd-upskill/fixtures/dedup-reference.md b/ai-evals/aidd-upskill/fixtures/dedup-reference.md new file mode 100644 index 00000000..7cef9484 --- /dev/null +++ b/ai-evals/aidd-upskill/fixtures/dedup-reference.md @@ -0,0 +1,10 @@ +# Name Constraints + +The `name` field in a SKILL.md frontmatter must be: + +- lowercase alphanumeric with hyphens +- 1-64 characters +- prefixed with `aidd-` +- must match the parent directory name + +This is the canonical definition of `SkillName` constraints. diff --git a/ai-evals/aidd-upskill/fixtures/dedup-skill.md b/ai-evals/aidd-upskill/fixtures/dedup-skill.md new file mode 100644 index 00000000..95318512 --- /dev/null +++ b/ai-evals/aidd-upskill/fixtures/dedup-skill.md @@ -0,0 +1,24 @@ +--- +name: aidd-example-dedup +description: Example skill used to test deduplication detection. Use when running dedup eval tests. +--- + +# aidd-example-dedup + +## Skill Structure + +Skills follow this directory layout: + +``` +ai/skills/aidd-/ +โ”œโ”€โ”€ SKILL.md # Required: frontmatter + instructions +โ”œโ”€โ”€ scripts/ # Optional: CLI tools +โ”œโ”€โ”€ references/ # Optional: detailed reference docs +โ””โ”€โ”€ assets/ # Optional: templates, data files +``` + +Frontmatter must include `name` and `description`. The `name` field must be +lowercase alphanumeric with hyphens, 1-64 characters, prefixed with `aidd-`, +and must match the parent directory name. + +import ai-evals/aidd-upskill/fixtures/dedup-reference.md diff --git a/ai-evals/aidd-upskill/fixtures/sample-format-skill.md b/ai-evals/aidd-upskill/fixtures/sample-format-skill.md new file mode 100644 index 00000000..32f013ce --- /dev/null +++ b/ai-evals/aidd-upskill/fixtures/sample-format-skill.md @@ -0,0 +1,30 @@ +--- +name: aidd-format-changelog +description: Format a list of git commits into a structured markdown changelog. Use when generating release notes or changelogs from commit history. +--- + +# aidd-format-changelog + +Given a list of raw git commit messages, produce a markdown changelog grouped +by conventional commit type (feat, fix, chore, etc.) with each entry on its +own line. + +## Steps + +1. Parse each commit message to extract type, scope, and description. +2. Group entries by type. +3. Render the grouped entries as a markdown changelog. + +## Example + +Input: `["feat(auth): add OAuth login", "fix(api): handle 429 rate limit"]` + +Output: + +```md +## Features +- **auth**: add OAuth login + +## Bug Fixes +- **api**: handle 429 rate limit +``` diff --git a/ai-evals/aidd-upskill/function-test.sudo b/ai-evals/aidd-upskill/function-test.sudo new file mode 100644 index 00000000..e7cc9389 --- /dev/null +++ b/ai-evals/aidd-upskill/function-test.sudo @@ -0,0 +1,21 @@ +# function-test.sudo + +import 'ai/skills/aidd-upskill/SKILL.md' + +userPrompt = """ +Run the Function Test on the skill described in +ai-evals/aidd-upskill/fixtures/sample-format-skill.md. + +Answer all five Function Test questions: +1. What is f? Name it. +2. What varies? (parameters) +3. What is constant? (defaults) +4. Is f deterministic? +5. Is the result independently useful and recomposable? +""" + +- Given the skill description maps raw git commits to a structured markdown changelog, should name f as something like "formatChangelog" or "groupCommitsByType" โ€” a clear verb-based name +- Given the skill accepts a list of commit messages as input, should identify the commit list as the parameter (what varies per caller) +- Given every caller uses the same grouping logic and markdown rendering, should identify the grouping strategy and output format as defaults (what is constant) +- Given the formatting logic is rule-based and produces the same output for the same input, should conclude that f is deterministic and therefore better suited to a CLI tool than an AI prompt +- Given a deterministic f verdict, should state the result in terms of the CLI vs AI prompt constraint from the skill (i.e. deterministic logic => CLI tool or compiled Bun bundle) diff --git a/ai/commands/aidd-upskill.md b/ai/commands/aidd-upskill.md new file mode 100644 index 00000000..4596f8a7 --- /dev/null +++ b/ai/commands/aidd-upskill.md @@ -0,0 +1,13 @@ +## ๐Ÿ› ๏ธ Upskill โ€” Create or Review Skills + +Use `ai/skills/aidd-upskill/SKILL.md` to create a new agent skill following the AgentSkills.io specification. + +Constraints { + Before beginning, read and respect the constraints in ai/skills/aidd-please/SKILL.md. +} + +## ๐Ÿ” Review Skill + +Use `ai/skills/aidd-upskill/SKILL.md` to review an existing skill against the quality criteria in this guide. + +Run: `/aidd-upskill review [path-to-skill]` diff --git a/ai/commands/index.md b/ai/commands/index.md index 52461906..db8fe6ca 100644 --- a/ai/commands/index.md +++ b/ai/commands/index.md @@ -52,6 +52,12 @@ Write correct riteway ai prompt evals for multi-step tool-calling flows. Use whe *No description available* +### ๐Ÿ› ๏ธ /aidd-upskill + +**File:** `aidd-upskill.md` + +Create and review AIDD skills following the AgentSkills.io specification and SudoLang authoring patterns. + ### Commit **File:** `commit.md` diff --git a/ai/skills/aidd-agent-orchestrator/SKILL.md b/ai/skills/aidd-agent-orchestrator/SKILL.md index 9d7d05a9..4810ff52 100644 --- a/ai/skills/aidd-agent-orchestrator/SKILL.md +++ b/ai/skills/aidd-agent-orchestrator/SKILL.md @@ -24,6 +24,7 @@ Agents { javascript-io-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 + aidd-upskill: 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/skills/aidd-please/SKILL.md b/ai/skills/aidd-please/SKILL.md index 142c9f69..eb7ddabb 100644 --- a/ai/skills/aidd-please/SKILL.md +++ b/ai/skills/aidd-please/SKILL.md @@ -45,6 +45,7 @@ Commands { ๐Ÿ“Š /aidd-churn - rank files by hotspot score (LoC ร— churn ร— complexity) to identify prime candidates for refactoring ๐Ÿงช /user-test - use /aidd-user-testing to generate human and AI agent test scripts from user journeys ๐Ÿค– /run-test - execute AI agent test script in real browser with screenshots + ๐Ÿ› ๏ธ /aidd-upskill - create a new agent skill using AgentSkills.io spec and SudoLang ๐Ÿ› /aidd-fix - fix a bug or implement review feedback following the full AIDD fix process ๐Ÿงช /aidd-riteway-ai - write correct riteway ai prompt evals for multi-step tool-calling flows } diff --git a/ai/skills/aidd-upskill/README.md b/ai/skills/aidd-upskill/README.md new file mode 100644 index 00000000..8a057aa8 --- /dev/null +++ b/ai/skills/aidd-upskill/README.md @@ -0,0 +1,34 @@ +# aidd-upskill + +Creates and reviews AIDD skills โ€” the reusable instruction modules that guide +agent behavior across a project. + +## Why + +Skills written without a clear structure accumulate bloat, mix concerns, and +become hard to maintain. `aidd-upskill` applies a consistent authoring +standard: each skill is a named function with defined inputs and outputs, +sized to stay concise, and organized for progressive disclosure. + +## Commands + +``` +/aidd-upskill create [name] +``` + +Scaffolds a new skill at `aidd-custom/skills/aidd-[name]/SKILL.md` with the required +frontmatter, sections, and directory layout. + +``` +/aidd-upskill review [target] +``` + +Evaluates an existing skill against authoring criteria: function test, +required sections, size thresholds, command separation, and README quality. +Reports issues and a pass/fail verdict. + +## When to use + +- Creating a new skill from scratch in `ai/skills/` or `aidd-custom/skills/` +- Reviewing or refactoring an existing skill for quality and consistency +- Checking whether a candidate abstraction is ready to become a named skill diff --git a/ai/skills/aidd-upskill/SKILL.md b/ai/skills/aidd-upskill/SKILL.md new file mode 100644 index 00000000..5aa87bfe --- /dev/null +++ b/ai/skills/aidd-upskill/SKILL.md @@ -0,0 +1,134 @@ +--- +name: aidd-upskill +description: Guide for crafting high-quality AIDD skills. Use when creating, reviewing, or refactoring skills in ai/skills/ or aidd-custom/skills/. +--- + +# aidd-upskill + +## Role + +Expert skill author. Craft skills that are clear, minimal, and recomposable, giving agents exactly the context they need โ€” nothing more. + +**Skill components:** `ai/skills/aidd-sudolang-syntax`, `ai/skills/aidd-rtc` + +Constraints { + Prefer natural language in markdown format + Use SudoLang interfaces, pattern matching, and /commands for formal specification + (logic is deterministic) => CLI tool or compiled Bun bundle + (logic requires judgment) => AI prompt + (a candidate abstraction cannot be named) => it is not an abstraction yet + (two or more skills share the same f) => extract a shared abstraction +} + +Commands { + /aidd-upskill create [name] โ€” scaffold a new skill at aidd-custom/skills/aidd-[name]/SKILL.md + /aidd-upskill review [target] โ€” evaluate a skill against the criteria in this guide +} + +> This skill is itself an example of the structure it prescribes. + +import references/types.md +import references/process.md + +## Process + +The `createSkill` and `reviewSkill` pipelines โ€” including all step definitions โ€” are defined in +`references/process.md` (imported above). Read that file for the full authoring and review +workflows. + +--- + +## Skill Structure + +``` +ai/skills/aidd-/ +โ”œโ”€โ”€ SKILL.md # Required: frontmatter + instructions +โ”œโ”€โ”€ README.md # Optional: what the skill is, why it's useful, command reference +โ”œโ”€โ”€ scripts/ # Optional: CLI tools +โ”œโ”€โ”€ references/ # Optional: detailed reference docs +โ””โ”€โ”€ assets/ # Optional: templates, data files +``` + +## Progressive Disclosure + +1. `name` + `description` โ€” loaded at startup for all skills +2. Full `SKILL.md` body โ€” loaded on activation +3. `scripts/`, `references/`, `assets/` โ€” loaded on demand + +Keep `SKILL.md` concise โ€” run `validate-skill` to check thresholds. Move reference material to `references/`. Use `import $referenceFile` to link it. + +--- + +## A Skill Is a Function + +``` +f: Input โ†’ Output +``` + +Every skill maps input context to output or action. Use this as the primary design lens. + +### Abstraction + +Two skills sharing the same `f` should share the same abstraction: + +- **Generalization:** extract the shared `f`, name it, hide it +- **Specialization:** expose only what differs as parameters + +``` +f: A โ†’ B +g: B โ†’ C +h: A โ†’ C โ† h hides B. This is a good abstraction. +``` + +> "Simplicity is removing the obvious and adding the meaningful." โ€” John Maeda + +### The Function Test + +1. What is `f`? Name it. If you can't name it, it's not an abstraction yet. +2. What varies? Those are the parameters โ€” expose them. +3. What is constant? Those are the defaults โ€” hide them. +4. Is `f` deterministic? See CLI vs. AI prompt in Constraints above. +5. Is the result independently useful and recomposable? If not, it's inlining, not abstraction. + +### Default Parameters + +Use defaults wherever the default is obvious. Callers supply only what is meaningfully different. If every caller passes the same value, it's a default waiting to be named. + +--- + +## Eval Tests + +Use Riteway AI to write eval tests for skill commands. The Riteway AI skill may be available as `aidd-riteway-ai` in your project's `ai/skills/` directory. + +**Core principle:** never mix thinking and effects in a single `/command`. Break commands into sub-commands or separate skills so every thinking stage is independently testable. + +``` +(command involves thinking + side effects) => split into sub-commands +(command is a pure thinking stage) => write an eval test +(command is a side effect only) => skip the test +``` + +### Eval Test Structure + +Tests are `.sudo` files using SudoLang syntax: + +```` +# my-skill-test.sudo + +import 'path/to/skill.md' + +userPrompt = """ + +""" + +- Given , should +- Given , should +```` + +Run with: + +```shell +riteway ai path/to/my-skill-test.sudo +``` + +Defaults: 4 passes, 75% pass rate threshold, claude agent. diff --git a/ai/skills/aidd-upskill/index.md b/ai/skills/aidd-upskill/index.md new file mode 100644 index 00000000..288729ec --- /dev/null +++ b/ai/skills/aidd-upskill/index.md @@ -0,0 +1,22 @@ +# aidd-upskill + +This index provides an overview of the contents in this directory. + +## Subdirectories + +### ๐Ÿ“ references/ + +See [`references/index.md`](./references/index.md) for contents. + +### ๐Ÿ“ scripts/ + +See [`scripts/index.md`](./scripts/index.md) for contents. + +## Files + +### aidd-upskill + +**File:** `SKILL.md` + +Guide for crafting high-quality AIDD skills. Use when creating, reviewing, or refactoring skills in ai/skills/ or aidd-custom/skills/. + diff --git a/ai/skills/aidd-upskill/references/index.md b/ai/skills/aidd-upskill/references/index.md new file mode 100644 index 00000000..e4d019d3 --- /dev/null +++ b/ai/skills/aidd-upskill/references/index.md @@ -0,0 +1,18 @@ +# references + +This index provides an overview of the contents in this directory. + +## Files + +### Skill Creation Process + +**File:** `process.md` + +*No description available* + +### Types & Interfaces + +**File:** `types.md` + +*No description available* + diff --git a/ai/skills/aidd-upskill/references/process.md b/ai/skills/aidd-upskill/references/process.md new file mode 100644 index 00000000..343938ac --- /dev/null +++ b/ai/skills/aidd-upskill/references/process.md @@ -0,0 +1,93 @@ +# Skill Creation Process + +## Pipeline + +``` +createSkill(userRequest) { + gatherRequirements + |> nameSkill + |> /aidd-rtc --compact + |> buildPlan + |> presentPlan + |> draftSkillMd + |> writeSkill + |> writeReadme + |> validate + |> reportMetrics +} +``` + +## Steps + +**gatherRequirements(userRequest)** +1. discoverRelatedSkills โ€” search `$projectRoot/ai/` and `$projectRoot/aidd-custom/` for SKILL.md, `.mdc`, `.md` files; read frontmatter descriptions; identify overlap or complementary skills +2. researchBestPractices โ€” use web search to find best practices for the domain; summarize findings +3. Infer requirements from the above context. Do not ask clarifying questions or block on user input. Use a judge to evaluate completeness: yes โ†’ proceed; no โ†’ state gaps as explicit assumptions and proceed. + +Infer answers to these questions from context rather than asking the user: +- What problem does this skill solve? +- What are its inputs and outputs? +- Any technical constraints or requirements? +- Should it `alwaysApply`? (recommend yes only if it applies to nearly every task) + +**nameSkill(topic)** +- Use verb or role-based noun form (e.g., `aidd-format-code`, `aidd-upskill`) +- Validate against `SkillName` type constraints + +**buildPlan() => SkillPlan** +Produce a `SkillPlan` struct (see `references/types.md`). + +**presentPlan(plan: SkillPlan)** +Show the full plan, then run a self-validating quality gate โ€” do not await user approval: +1. Run `/aidd-review` on the plan +2. Issues found => run `/aidd-fix` loop until all issues are resolved +3. Proceed to `draftSkillMd` + +**draftSkillMd(plan: SkillPlan)** +- Write frontmatter: `name` + `description` required; add `metadata.alwaysApply` if needed +- Write body with all `RequiredSections` +- If body will exceed the line threshold (run `validate-skill` to check), extract content to `references/` and use `import $referenceFile` + +**writeSkill(skillMd)** +- Write to `$projectRoot/aidd-custom/skills/${skillName}/SKILL.md` +- Create `scripts/`, `references/`, or `assets/` directories only if planned + +**writeReadme(skillMd)** +- Write `README.md` in the skill directory +- Include: what the skill is, why it is useful, command reference with usage examples +- Exclude: implementation details, process narratives, pipeline descriptions + +**validate** +```bash +ai/skills/aidd-upskill/scripts/validate-skill ./path-to-skill-directory +# If skills-ref is available: +skills-ref validate ./path-to-skill-directory +``` + +**reportMetrics** +Report `SizeMetrics` and any threshold warnings to the user. + +## Skill Review Process + +``` +reviewSkill(target) { + readSkill(target) + |> runFunctionTest + |> checkRequiredSections + |> checkSizeMetrics + |> checkCommandSeparation + |> checkReadme + |> deduplicate() + |> /aidd-rtc --compact + |> reportFindings +} +``` + +**runFunctionTest** โ€” apply the 5-question Function Test from SKILL.md +**checkRequiredSections** โ€” verify all `RequiredSections` are present +**checkSizeMetrics** โ€” run `validate-skill` and report warnings +**checkCommandSeparation** โ€” verify no command mixes thinking and side effects +**checkReadme** โ€” verify README.md exists and contains what/why/commands; flag if it contains implementation details or process narratives +**deduplicate()** โ€” find every instance of repeated information across SKILL.md and its references; flag each duplicate and identify where the single source of truth should live; use `/aidd-rtc --compact` to reason about the canonical location +**/aidd-rtc --compact** โ€” synthesize all findings into a holistic judgment before rendering the verdict; independently testable as a pure thinking stage +**reportFindings** โ€” produce a per-check pass/fail table (one row per check: runFunctionTest, checkRequiredSections, checkSizeMetrics, checkCommandSeparation, checkReadme, deduplicate) with columns for check name, result (โœ…/โš ๏ธ/โŒ), and detail; conclude with an overall verdict diff --git a/ai/skills/aidd-upskill/references/types.md b/ai/skills/aidd-upskill/references/types.md new file mode 100644 index 00000000..d0afa4f8 --- /dev/null +++ b/ai/skills/aidd-upskill/references/types.md @@ -0,0 +1,75 @@ +# Types & Interfaces + +## Types + +``` +type SkillName = string( + 1-64 chars, + lowercase alphanumeric + hyphens, + no leading/trailing/consecutive hyphens, + must match parent directory name, + prefix: "aidd-", + verb or role-based noun +) + +type SkillDescription = string( + 1-1024 chars, + describes what the skill does AND when to use it, + precise enough for an agent to activate on description alone +) +``` + +## SizeMetrics + +``` +SizeMetrics { + frontmatterTokens: number // run validate-skill for current thresholds + bodyLines: number // run validate-skill for current thresholds + bodyTokens: number // run validate-skill for current thresholds +} +``` + +## SkillPlan + +``` +SkillPlan { + name: SkillName + purpose: SkillDescription + alwaysApply: boolean // preload on project init? Use sparingly. + relatedSkills[] // existing skills found during discovery + bestPractices[] // findings from research + proposedSections[] // planned SKILL.md structure + optionalDirs: ["scripts" | "references" | "assets"] + sizeEstimate: SizeMetrics +} +``` + +## Frontmatter + +``` +Frontmatter { + name: SkillName // required + description: SkillDescription // required + license // optional + compatibility: string(1-500) // optional, environment requirements + metadata {} // optional, AIDD extensions + allowed-tools // optional, space-delimited tool list +} +``` + +### AIDD Extensions via `metadata` + +`metadata.alwaysApply: "true"` preloads the full SKILL.md on project init. +Use only for skills that apply to nearly every task (e.g., coding standards). +Task-specific skills should activate on demand, not preload. + +## RequiredSections + +Every generated SKILL.md body must include: + +``` +RequiredSections { + "# Title" // skill name as heading + "## Steps" | "## Process" // ordered execution instructions +} +``` diff --git a/ai/skills/aidd-upskill/scripts/index.md b/ai/skills/aidd-upskill/scripts/index.md new file mode 100644 index 00000000..e14754b2 --- /dev/null +++ b/ai/skills/aidd-upskill/scripts/index.md @@ -0,0 +1,8 @@ +# scripts + +This index provides an overview of the contents in this directory. + +| File | Description | +|------|-------------| +| `validate-skill.js` | Validator module and CLI entry point โ€” checks a skill directory for name validity, frontmatter structure, and size thresholds | +| `validate-skill.test.js` | Unit tests for the validator | diff --git a/ai/skills/aidd-upskill/scripts/validate-skill.js b/ai/skills/aidd-upskill/scripts/validate-skill.js new file mode 100644 index 00000000..e1c10ddb --- /dev/null +++ b/ai/skills/aidd-upskill/scripts/validate-skill.js @@ -0,0 +1,170 @@ +/** + * Validate an AgentSkills.io SKILL.md file. + * + * Usage: validate-skill ./path-to-skill-directory + * (compiled to a binary via `bun build --compile`) + * For development: bun run validate-skill.js ./path-to-skill-directory + */ + +import { readFileSync } from "node:fs"; +import { basename, join } from "node:path"; +import { fileURLToPath } from "url"; +import yaml from "js-yaml"; + +export const parseSkillMd = (content) => { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\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 (!name.startsWith("aidd-")) + errors.push("Name must start with 'aidd-' prefix"); + 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), +}); + +const ALLOWED_FRONTMATTER_KEYS = new Set([ + "name", + "description", + "license", + "compatibility", + "metadata", + "allowed-tools", +]); + +export const validateFrontmatterKeys = (frontmatterObj) => { + const errors = []; + for (const key of Object.keys(frontmatterObj)) { + if (!ALLOWED_FRONTMATTER_KEYS.has(key)) { + errors.push(`Unknown frontmatter key: ${key}`); + } + } + return errors; +}; + +export const checkThresholds = (metrics) => { + const errors = []; + const warnings = []; + if (metrics.frontmatterTokens >= 100) + warnings.push("Frontmatter reaches or exceeds 100 token guideline"); + if (metrics.bodyLines >= 500) + errors.push( + "Body exceeds 500 line spec limit - split into reference files", + ); + else if (metrics.bodyLines >= 160) + warnings.push("Body exceeds 160 line guideline"); + if (metrics.bodyTokens >= 5000) + warnings.push("Body exceeds 5000 token spec guideline"); + return { errors, warnings }; +}; + +/** + * Parse and validate SKILL.md content against a known directory name. + * Returns { errors, warnings, metrics }. + */ +export const validateSkillContent = (content, dirName) => { + const { frontmatter, body } = parseSkillMd(content); + const errors = []; + let parsedFrontmatter; + try { + parsedFrontmatter = frontmatter ? yaml.load(frontmatter) : {}; + } catch (e) { + errors.push(`Invalid YAML in frontmatter: ${e.message}`); + const metrics = calculateMetrics(frontmatter, body); + const { errors: thresholdErrors, warnings } = checkThresholds(metrics); + return { errors: [...errors, ...thresholdErrors], metrics, warnings }; + } + if ( + typeof parsedFrontmatter !== "object" || + parsedFrontmatter === null || + Array.isArray(parsedFrontmatter) + ) { + errors.push("Frontmatter must be a YAML mapping (key-value object)"); + const metrics = calculateMetrics(frontmatter, body); + const { errors: thresholdErrors, warnings } = checkThresholds(metrics); + return { errors: [...errors, ...thresholdErrors], metrics, warnings }; + } + const name = + typeof parsedFrontmatter.name === "string" ? parsedFrontmatter.name : ""; + const description = + typeof parsedFrontmatter.description === "string" + ? parsedFrontmatter.description + : ""; + errors.push(...validateName(name, dirName)); + errors.push(...validateFrontmatterKeys(parsedFrontmatter)); + if (!description) errors.push("Description is required"); + else if (description.length > 1024) + errors.push("Description must be 1024 characters or fewer"); + const metrics = calculateMetrics(frontmatter, body); + const { errors: thresholdErrors, warnings } = checkThresholds(metrics); + return { errors: [...errors, ...thresholdErrors], metrics, warnings }; +}; + +/** Resolves whether this module is the entry point (testable; mirrors CLI guard). */ +export const resolveIsMainEntry = ({ main, argv1, moduleUrl }) => + typeof main !== "undefined" ? main : argv1 === fileURLToPath(moduleUrl); + +const isMain = resolveIsMainEntry({ + argv1: process.argv[1], + main: import.meta.main, + moduleUrl: import.meta.url, +}); + +if (isMain) { + const skillDir = process.argv[2]; + if (!skillDir) { + console.error("Usage: validate-skill "); + process.exit(1); + } + + const dirName = basename(skillDir); + let content; + try { + content = readFileSync(join(skillDir, "SKILL.md"), "utf8"); + } catch { + console.error(`Error: could not read SKILL.md in ${skillDir}`); + process.exit(1); + } + + const { errors, metrics, warnings } = validateSkillContent(content, dirName); + + console.log(`\nValidating: ${skillDir}\n`); + console.log("Metrics:"); + console.log(` frontmatterTokens : ${metrics.frontmatterTokens}`); + console.log(` bodyLines : ${metrics.bodyLines}`); + console.log(` bodyTokens : ${metrics.bodyTokens}`); + + if (warnings.length > 0) { + console.log("\nWarnings:"); + for (const w of warnings) console.log(` โš  ${w}`); + } + + if (errors.length > 0) { + console.log("\nErrors:"); + for (const e of errors) console.log(` โœ– ${e}`); + process.exit(1); + } + + console.log( + warnings.length > 0 + ? "\nโš  Validation passed with warnings." + : "\nโœ” Validation passed.", + ); +} diff --git a/ai/skills/aidd-upskill/scripts/validate-skill.test.js b/ai/skills/aidd-upskill/scripts/validate-skill.test.js new file mode 100644 index 00000000..a140e111 --- /dev/null +++ b/ai/skills/aidd-upskill/scripts/validate-skill.test.js @@ -0,0 +1,867 @@ +import { fileURLToPath } from "url"; +import { assert } from "riteway/vitest"; +import { describe, test } from "vitest"; + +import { + calculateMetrics, + checkThresholds, + parseSkillMd, + resolveIsMainEntry, + validateFrontmatterKeys, + validateName, + validateSkillContent, +} from "./validate-skill.js"; + +describe("resolveIsMainEntry", () => { + const moduleUrl = + "file:///workspace/ai/skills/aidd-upskill/scripts/validate-skill.js"; + const modulePath = fileURLToPath(moduleUrl); + + test("uses import.meta.main when defined (Bun)", () => { + assert({ + given: "import.meta.main is true and argv1 does not match module path", + should: "return true so compiled Bun binaries still run the CLI", + actual: resolveIsMainEntry({ + main: true, + argv1: "/tmp/stale-build-path", + moduleUrl, + }), + expected: true, + }); + + assert({ + given: "import.meta.main is false", + should: "return false", + actual: resolveIsMainEntry({ + main: false, + argv1: modulePath, + moduleUrl, + }), + expected: false, + }); + }); + + test("falls back to argv comparison when main is undefined (Node)", () => { + assert({ + given: "main is undefined and argv1 matches this module URL path", + should: "return true", + actual: resolveIsMainEntry({ + main: undefined, + argv1: modulePath, + moduleUrl, + }), + expected: true, + }); + + assert({ + given: "main is undefined and argv1 does not match", + should: "return false", + actual: resolveIsMainEntry({ + main: undefined, + argv1: "/other/script.js", + moduleUrl, + }), + expected: false, + }); + }); +}); + +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, + expected: "name: my-skill\ndescription: A test skill.", + }); + + assert({ + given: "SKILL.md content with valid frontmatter", + should: "return body without frontmatter", + actual: result.body, + expected: "# My Skill\n\nBody content here.", + }); + }); + + 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, + }); + }); + + test("CRLF line endings", () => { + const content = + "---\r\nname: aidd-my-skill\r\ndescription: A test skill.\r\n---\r\n# My Skill\r\n\r\nBody content here."; + const result = parseSkillMd(content); + + assert({ + given: "SKILL.md content with CRLF line endings", + should: "return parsed frontmatter string", + actual: result.frontmatter, + expected: "name: aidd-my-skill\r\ndescription: A test skill.", + }); + + assert({ + given: "SKILL.md content with CRLF line endings", + should: "return body without frontmatter block", + actual: result.body, + expected: "# My Skill\r\n\r\nBody content here.", + }); + }); +}); + +describe("validateName", () => { + test("valid names", () => { + assert({ + given: "a valid aidd-prefixed lowercase hyphenated name", + should: "return no errors", + actual: validateName("aidd-format-code", "aidd-format-code"), + expected: [], + }); + }); + + test("missing aidd- prefix", () => { + const errors = validateName("format-code", "format-code"); + + assert({ + given: "a name without the aidd- prefix", + should: "return an error requiring the aidd- prefix", + actual: errors[0], + expected: "Name must start with 'aidd-' prefix", + }); + }); + + test("uppercase letters", () => { + const errors = validateName("Format-Code", "Format-Code"); + + assert({ + given: "a name with uppercase letters", + should: "return an error requiring lowercase alphanumeric + hyphens", + actual: errors[0], + expected: "Name must be lowercase alphanumeric + hyphens only", + }); + + assert({ + given: "a name with uppercase letters", + should: "also return an error requiring the aidd- prefix", + actual: errors[1], + expected: "Name must start with 'aidd-' prefix", + }); + }); + + test("leading hyphen", () => { + const errors = validateName("-my-skill", "-my-skill"); + + assert({ + given: "a name starting with a hyphen", + should: "return an error requiring the aidd- prefix", + actual: errors[0], + expected: "Name must start with 'aidd-' prefix", + }); + + assert({ + given: "a name starting with a hyphen", + should: "return an error about leading/trailing hyphen", + actual: errors[1], + expected: "Name must not start or end with hyphen", + }); + }); + + test("trailing hyphen", () => { + const errors = validateName("my-skill-", "my-skill-"); + + assert({ + given: "a name ending with a hyphen", + should: "return an error requiring the aidd- prefix", + actual: errors[0], + expected: "Name must start with 'aidd-' prefix", + }); + + assert({ + given: "a name ending with a hyphen", + should: "return an error about leading/trailing hyphen", + actual: errors[1], + expected: "Name must not start or end with hyphen", + }); + }); + + test("consecutive hyphens", () => { + const errors = validateName("my--skill", "my--skill"); + + assert({ + given: "a name with consecutive hyphens", + should: "return an error requiring the aidd- prefix", + actual: errors[0], + expected: "Name must start with 'aidd-' prefix", + }); + + assert({ + given: "a name with consecutive hyphens", + should: "return an error about consecutive hyphens", + actual: errors[1], + expected: "Name must not contain consecutive hyphens", + }); + }); + + test("too long", () => { + const longName = "a".repeat(65); + const errors = validateName(longName, longName); + + assert({ + given: "a name longer than 64 characters", + should: "return an error about name length", + actual: errors[0], + expected: "Name must be 1-64 characters", + }); + + assert({ + given: "a name longer than 64 characters", + should: "return an error requiring the aidd- prefix", + actual: errors[1], + expected: "Name must start with 'aidd-' prefix", + }); + }); + + test("empty name", () => { + const errors = validateName("", "some-dir"); + + assert({ + given: "an empty name", + should: "return an error about name length", + actual: errors[0], + expected: "Name must be 1-64 characters", + }); + + assert({ + given: "an empty name", + should: "return an error requiring the aidd- prefix", + actual: errors[1], + expected: "Name must start with 'aidd-' prefix", + }); + + assert({ + given: "an empty name", + should: "return an error about mismatched directory name", + actual: errors[2], + expected: "Name must match directory name", + }); + }); + + test("directory mismatch", () => { + const errors = validateName("my-skill", "other-dir"); + + assert({ + given: "a name that does not match the directory name", + should: "return an error requiring the aidd- prefix", + actual: errors[0], + expected: "Name must start with 'aidd-' prefix", + }); + + assert({ + given: "a name that does not match the directory name", + should: "return an error about mismatched directory name", + actual: errors[1], + expected: "Name must match directory name", + }); + }); +}); + +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(31 chars / 4) = 8", + actual: result.frontmatterTokens, + expected: 8, + }); + + 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(22 chars / 4) = 6", + actual: result.bodyTokens, + expected: 6, + }); + }); +}); + +describe("checkThresholds", () => { + test("all within limits", () => { + const metrics = { frontmatterTokens: 50, bodyLines: 100, bodyTokens: 3000 }; + const result = checkThresholds(metrics); + + assert({ + given: "metrics within all thresholds", + should: "return no errors", + actual: result.errors, + expected: [], + }); + + assert({ + given: "metrics within all thresholds", + should: "return no warnings", + actual: result.warnings, + expected: [], + }); + }); + + test("frontmatter too large", () => { + const metrics = { frontmatterTokens: 100, bodyLines: 50, bodyTokens: 1000 }; + const result = checkThresholds(metrics); + + assert({ + given: "frontmatter at 100 tokens", + should: "return a frontmatter warning", + actual: result.warnings.some((w) => w.includes("Frontmatter")), + expected: true, + }); + + assert({ + given: "frontmatter at 100 tokens", + should: "not produce a hard error", + actual: result.errors, + expected: [], + }); + }); + + test("body exceeds 160 lines", () => { + const metrics = { frontmatterTokens: 10, bodyLines: 160, bodyTokens: 1000 }; + const result = checkThresholds(metrics); + + assert({ + given: "body at 160 lines", + should: "return a 160-line warning", + actual: result.warnings.some((w) => w.includes("160")), + expected: true, + }); + + assert({ + given: "body at 160 lines", + should: "not produce a hard error", + actual: result.errors, + expected: [], + }); + }); + + test("body exceeds 500 lines", () => { + const metrics = { frontmatterTokens: 10, bodyLines: 500, bodyTokens: 1000 }; + const result = checkThresholds(metrics); + + assert({ + given: "body at 500 lines", + should: "return a 500-line hard error", + actual: result.errors.some((e) => e.includes("500")), + expected: true, + }); + + assert({ + given: "body at 500 lines", + should: "not duplicate 500-line message in warnings", + actual: result.warnings.some((w) => w.includes("500")), + expected: false, + }); + }); + + test("body exceeds 5000 tokens", () => { + const metrics = { frontmatterTokens: 10, bodyLines: 50, bodyTokens: 5000 }; + const result = checkThresholds(metrics); + + assert({ + given: "body at 5000 tokens", + should: "return a token warning", + actual: result.warnings.some((w) => w.includes("5000")), + expected: true, + }); + + assert({ + given: "body at 5000 tokens", + should: "not produce a hard error", + actual: result.errors, + expected: [], + }); + }); + + test("body at 159 lines (just below soft threshold)", () => { + const metrics = { frontmatterTokens: 10, bodyLines: 159, bodyTokens: 1000 }; + const result = checkThresholds(metrics); + + assert({ + given: "body at 159 lines", + should: "return no body-line warning", + actual: result.warnings.filter((w) => w.includes("160")), + expected: [], + }); + + assert({ + given: "body at 159 lines", + should: "return no errors", + actual: result.errors, + expected: [], + }); + }); + + test("body at 499 lines (just below hard limit)", () => { + const metrics = { frontmatterTokens: 10, bodyLines: 499, bodyTokens: 1000 }; + const result = checkThresholds(metrics); + + assert({ + given: "body at 499 lines", + should: "return a 160-line soft warning", + actual: result.warnings.some((w) => w.includes("160")), + expected: true, + }); + + assert({ + given: "body at 499 lines", + should: "not produce a hard error", + actual: result.errors, + expected: [], + }); + }); + + test("frontmatter at 99 tokens (just below warning threshold)", () => { + const metrics = { frontmatterTokens: 99, bodyLines: 50, bodyTokens: 1000 }; + const result = checkThresholds(metrics); + + assert({ + given: "frontmatter at 99 tokens", + should: "return no frontmatter warning", + actual: result.warnings.filter((w) => w.includes("Frontmatter")), + expected: [], + }); + + assert({ + given: "frontmatter at 99 tokens", + should: "return no errors", + actual: result.errors, + expected: [], + }); + }); + + test("body at 4999 tokens (just below warning threshold)", () => { + const metrics = { frontmatterTokens: 10, bodyLines: 50, bodyTokens: 4999 }; + const result = checkThresholds(metrics); + + assert({ + given: "body at 4999 tokens", + should: "return no token warning", + actual: result.warnings.filter((w) => w.includes("5000")), + expected: [], + }); + + assert({ + given: "body at 4999 tokens", + should: "return no errors", + actual: result.errors, + expected: [], + }); + }); +}); + +describe("validateSkillContent", () => { + test("valid skill with matching dir name", () => { + const content = `--- +name: aidd-my-skill +description: A test skill. +--- +# My Skill + +Body content here.`; + + const result = validateSkillContent(content, "aidd-my-skill"); + + assert({ + given: "valid SKILL.md content and matching directory name", + should: "return no errors and computed metrics", + actual: result.errors, + expected: [], + }); + + assert({ + given: "valid SKILL.md content", + should: "return bodyLines metric", + actual: typeof result.metrics.bodyLines, + expected: "number", + }); + }); + + test("mismatched directory name", () => { + const content = `--- +name: aidd-my-skill +description: A test skill. +--- +# My Skill`; + + const result = validateSkillContent(content, "other-dir"); + + assert({ + given: "SKILL.md name does not match directory name", + should: "return an error", + actual: result.errors.length > 0, + expected: true, + }); + }); + + test("missing frontmatter name", () => { + const content = `--- +description: No name here. +--- +# My Skill`; + + const result = validateSkillContent(content, ""); + + assert({ + given: "frontmatter without a name field", + should: "return an error", + actual: result.errors.length > 0, + expected: true, + }); + }); + + test("valid description passes without description-related errors", () => { + const content = `--- +name: aidd-my-skill +description: A valid skill description. +--- +# My Skill + +Body content here.`; + + const result = validateSkillContent(content, "aidd-my-skill"); + + assert({ + given: "a skill with a valid description", + should: "return no errors", + actual: result.errors, + expected: [], + }); + }); + + test("missing description field fails with error", () => { + const content = `--- +name: aidd-my-skill +--- +# My Skill + +Body content here.`; + + const result = validateSkillContent(content, "aidd-my-skill"); + + assert({ + given: "a skill with no description field", + should: "return a description required error", + actual: result.errors.some((e) => e.includes("Description is required")), + expected: true, + }); + }); + + test("empty string description field fails with error", () => { + const content = `--- +name: aidd-my-skill +description: "" +--- +# My Skill + +Body content here.`; + + const result = validateSkillContent(content, "aidd-my-skill"); + + assert({ + given: "a skill with an empty string description", + should: "return a description required error", + actual: result.errors.some((e) => e.includes("Description is required")), + expected: true, + }); + }); + + test("description exceeding 1024 characters fails with error", () => { + const longDescription = "a".repeat(1025); + const content = `--- +name: aidd-my-skill +description: ${longDescription} +--- +# My Skill + +Body content here.`; + + const result = validateSkillContent(content, "aidd-my-skill"); + + assert({ + given: "a skill with a description longer than 1024 characters", + should: "return a description length error", + actual: result.errors.some((e) => + e.includes("Description must be 1024 characters or fewer"), + ), + expected: true, + }); + }); + + test("description of exactly 1024 characters passes without error", () => { + const maxDescription = "a".repeat(1024); + const content = `--- +name: aidd-my-skill +description: ${maxDescription} +--- +# My Skill + +Body content here.`; + + const result = validateSkillContent(content, "aidd-my-skill"); + + assert({ + given: "a skill with a description of exactly 1024 characters", + should: "not return a description length error", + actual: result.errors.some((e) => + e.includes("Description must be 1024 characters or fewer"), + ), + expected: false, + }); + }); + + test("quoted name field is stripped of quotes before validation", () => { + const content = `--- +name: "aidd-my-skill" +description: A test skill. +--- +# My Skill + +Body content here.`; + + const result = validateSkillContent(content, "aidd-my-skill"); + + assert({ + given: + 'a skill with name: "aidd-my-skill" (YAML double-quoted) and matching directory', + should: "return no name-related errors", + actual: result.errors, + expected: [], + }); + }); + + test("single-quoted name field is stripped of quotes before validation", () => { + const content = `--- +name: 'aidd-my-skill' +description: A test skill. +--- +# My Skill + +Body content here.`; + + const result = validateSkillContent(content, "aidd-my-skill"); + + assert({ + given: + "a skill with name: 'aidd-my-skill' (YAML single-quoted) and matching directory", + should: "return no name-related errors", + actual: result.errors, + expected: [], + }); + }); + + test("malformed YAML frontmatter returns an Invalid YAML error instead of throwing", () => { + const content = `--- +name: aidd-my-skill +description: [broken +--- +# My Skill + +Body content here.`; + + let result; + let threw = false; + try { + result = validateSkillContent(content, "aidd-my-skill"); + } catch { + threw = true; + } + + assert({ + given: "a skill with malformed YAML in frontmatter", + should: "not throw an exception", + actual: threw, + expected: false, + }); + + assert({ + given: "a skill with malformed YAML in frontmatter", + should: 'return an error containing "Invalid YAML"', + actual: result.errors.some((e) => e.includes("Invalid YAML")), + expected: true, + }); + }); + + test("unknown frontmatter key produces an error via validateSkillContent", () => { + const content = `--- +name: aidd-my-skill +description: A valid skill description. +unknown-key: some value +--- +# My Skill + +Body content here.`; + + const result = validateSkillContent(content, "aidd-my-skill"); + + assert({ + given: "a skill with an unknown frontmatter key", + should: "return an error naming the unknown key", + actual: result.errors.some((e) => + e.includes("Unknown frontmatter key: unknown-key"), + ), + expected: true, + }); + }); + + test("inline YAML comment on name field does not corrupt name validation", () => { + const content = `--- +name: aidd-my-skill # inline comment +description: A test skill. +--- +# My Skill + +Body content here.`; + + const result = validateSkillContent(content, "aidd-my-skill"); + + assert({ + given: + "a SKILL.md with an inline YAML comment on the name field (name: aidd-my-skill # comment)", + should: "validate name correctly and return no errors", + actual: result.errors, + expected: [], + }); + }); + + test("degenerate frontmatter that is a bare scalar returns a YAML mapping error without throwing", () => { + const content = `--- +just a string +--- +# My Skill + +Body content here.`; + + let result; + let threw = false; + try { + result = validateSkillContent(content, "aidd-my-skill"); + } catch { + threw = true; + } + + assert({ + given: + "a SKILL.md whose frontmatter is a bare YAML scalar (not a mapping)", + should: "not throw an exception", + actual: threw, + expected: false, + }); + + assert({ + given: + "a SKILL.md whose frontmatter is a bare YAML scalar (not a mapping)", + should: 'return an error containing "must be a YAML mapping"', + actual: result.errors.some((e) => e.includes("must be a YAML mapping")), + expected: true, + }); + }); +}); + +describe("validateFrontmatterKeys", () => { + test("all allowed keys present returns no errors", () => { + const frontmatterObj = { + name: "aidd-my-skill", + description: "A test skill.", + license: "MIT", + compatibility: "node >= 18", + metadata: { alwaysApply: "true" }, + "allowed-tools": "read write", + }; + + assert({ + given: "a frontmatter object with only allowed keys", + should: "return no errors", + actual: validateFrontmatterKeys(frontmatterObj), + expected: [], + }); + }); + + test("one unknown key returns one error naming the key", () => { + const frontmatterObj = { + name: "aidd-my-skill", + description: "A test skill.", + "unknown-key": "some value", + }; + + assert({ + given: "a frontmatter object with one unknown key", + should: "return one error naming the key", + actual: validateFrontmatterKeys(frontmatterObj), + expected: ["Unknown frontmatter key: unknown-key"], + }); + }); + + test("multiple unknown keys return multiple errors", () => { + const frontmatterObj = { + name: "aidd-my-skill", + description: "A test skill.", + "bad-key": "x", + "another-bad-key": "y", + }; + + const errors = validateFrontmatterKeys(frontmatterObj); + + assert({ + given: "a frontmatter object with two unknown keys", + should: "return two errors", + actual: errors.length, + expected: 2, + }); + + assert({ + given: "a frontmatter object with two unknown keys", + should: "name first unknown key in an error", + actual: errors.some((e) => e.includes("bad-key")), + expected: true, + }); + + assert({ + given: "a frontmatter object with two unknown keys", + should: "name second unknown key in an error", + actual: errors.some((e) => e.includes("another-bad-key")), + expected: true, + }); + }); +}); diff --git a/ai/skills/index.md b/ai/skills/index.md index 9bbe3c8f..e406f5ac 100644 --- a/ai/skills/index.md +++ b/ai/skills/index.md @@ -32,4 +32,5 @@ - aidd-tdd - Systematic test-driven development with proper test isolation. Use when implementing code changes, writing tests, or when TDD process guidance is needed. - aidd-timing-safe-compare - Security rule for timing-safe secret comparison. Use SHA3-256 hashing instead of timing-safe compare functions. Use when reviewing or implementing secret comparisons, token validation, CSRF tokens, or API key checks. - aidd-ui - Design beautiful and friendly user interfaces and experiences. Use when building UI components, styling, animations, accessibility, responsive design, or working with design systems. +- aidd-upskill - Guide for crafting high-quality AIDD skills. Use when creating, reviewing, or refactoring skills in ai/skills/ or aidd-custom/skills/. - aidd-user-testing - Generate human and AI agent test scripts from user journey specifications. Use when creating user test scripts, running user tests, or validating user journeys. diff --git a/package.json b/package.json index 3eae00af..5dca1a9c 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "url": "git+https://github.com/paralleldrive/aidd.git" }, "scripts": { + "build:validate-skill": "bun build --compile ai/skills/aidd-upskill/scripts/validate-skill.js --outfile ai/skills/aidd-upskill/scripts/validate-skill", "check-status": "[ -n \"$(git status --porcelain)\" ] && { echo 'โŒ Uncommitted changes'; exit 1; } || echo 'โœ… Git status is clean.'", "format": "npx @biomejs/biome format --write . && echo 'Format complete.'", "format:check": "npx @biomejs/biome check && echo 'Check complete.'", @@ -106,6 +107,7 @@ "release": "node release.js", "test": "vitest run && echo 'Test complete.' && npm run -s lint && npm run -s typecheck", "test:ai-eval": "riteway ai ai-evals/aidd-review/review-skill-test.sudo --runs 1 --threshold 75 --timeout 600000 --agent claude --color --save-responses", + "test:ai-eval:upskill": "riteway ai ai-evals/aidd-upskill/*.sudo --runs 1 --threshold 75 --timeout 600000 --agent claude --color --save-responses", "test:e2e": "vitest run **/*-e2e.test.js && echo 'E2E tests complete.'", "test:unit": "vitest run --exclude '**/*-e2e.test.js' && echo 'Unit tests complete.' && npm run -s lint && npm run -s typecheck", "toc": "doctoc README.md", diff --git a/tasks/aidd-upskill-epic.md b/tasks/aidd-upskill-epic.md new file mode 100644 index 00000000..47aebe93 --- /dev/null +++ b/tasks/aidd-upskill-epic.md @@ -0,0 +1,40 @@ +# aidd-upskill Epic + +**Goal**: A skill that guides agents to create and review high-quality AIDD skills. + +## Skill Design + +- Given an agent is designing a skill, should apply a function framing (`f: Input โ†’ Output`) as the primary design lens before committing to structure. +- Given two skills share the same `f`, should extract a shared abstraction rather than duplicating. +- Given a candidate abstraction cannot be named, should not be treated as an abstraction yet. +- Given a skill is being designed or reviewed, should apply the Function Test: name `f`, identify parameters vs defaults, determine CLI vs AI prompt, confirm recomposability. + +## Create Pipeline + +- Given the create pipeline documented in `ai/skills/aidd-upskill/references/process.md`, should treat `discoverRelatedSkills` and `researchBestPractices` as sub-steps inside `gatherRequirements`, not as separate top-level pipeline stages that would run before those outputs exist. +- Given `/aidd-upskill create [name]` is invoked, should infer requirements from context rather than blocking on user input; any gaps should be stated as explicit assumptions before proceeding. +- Given a plan has been built, should validate it via `/aidd-review` and resolve issues via `/aidd-fix` before drafting โ€” should not await explicit user approval. +- Given a skill is being created, should produce a `README.md` containing what the skill is, why it is useful, and a command reference with usage examples. + +## Review Pipeline + +- Given `/aidd-upskill review [target]` is invoked, should run all checks and produce a per-check pass/fail table with an overall verdict. +- Given a skill is under review, should scan SKILL.md and all reference files for duplicated information and identify the single source of truth for each. +- Given a skill README is under review, should flag if it is missing, lacks what/why/commands, or contains implementation details or process narratives. + +## README Authoring + +- Given a skill README is authored, should not contain implementation details, process pipeline descriptions, or internal narratives. + +## Validator CLI + +- Given `validate-skill` is run against a skill directory, should report name errors and size threshold warnings. +- Given `validate-skill` is executed as the program entry point (including when packaged as a Bun-compiled binary), should run the CLI and validate the target directory instead of treating the script as an imported library only. +- Given the module is loaded as a CLI entry point, should determine `isMain` by calling `resolveIsMainEntry` instead of duplicating its logic inline. +- Given a SKILL.md frontmatter field value contains an inline YAML comment (e.g. `name: aidd-my-skill # comment`), should validate `name` and `description` correctly without treating the comment as part of the value. +- Given `yaml.load` returns a non-object value for degenerate frontmatter (e.g. a bare scalar string), should return an error containing "must be a YAML mapping" without throwing. + +## Eval Tests + +- Given `runFunctionTest` is applied to a skill description, should correctly name `f`, distinguish parameters from defaults, and reach the correct CLI vs AI prompt verdict. +- Given `deduplicateWithCaveman()` is applied to a SKILL.md and reference file containing duplicated content, should identify the duplicate and name the canonical location. From 839ba4f1c933f38f66c968f6915bb9854622d6f0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 10 Apr 2026 22:10:06 +0000 Subject: [PATCH 2/4] fix(aidd-upskill): replace broken aidd-rtc references with think() from aidd-please - SKILL.md: update skill components to reference aidd-please (provides RTC think()) instead of non-existent aidd-rtc skill - references/process.md: replace /aidd-rtc --compact with think() --compact in both createSkill and reviewSkill pipelines and step descriptions - ai/commands/aidd-upskill.md: align format with other command files (# heading, /aidd-please reference, load-and-execute pattern) --- ai/commands/aidd-upskill.md | 12 +++++++----- ai/skills/aidd-upskill/SKILL.md | 2 +- ai/skills/aidd-upskill/references/process.md | 8 ++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/ai/commands/aidd-upskill.md b/ai/commands/aidd-upskill.md index 4596f8a7..25f4ab0a 100644 --- a/ai/commands/aidd-upskill.md +++ b/ai/commands/aidd-upskill.md @@ -1,13 +1,15 @@ -## ๐Ÿ› ๏ธ Upskill โ€” Create or Review Skills +# ๐Ÿ› ๏ธ /aidd-upskill -Use `ai/skills/aidd-upskill/SKILL.md` to create a new agent skill following the AgentSkills.io specification. +Load and execute the skill at `ai/skills/aidd-upskill/SKILL.md`. Constraints { - Before beginning, read and respect the constraints in ai/skills/aidd-please/SKILL.md. + Before beginning, read and respect the constraints in /aidd-please. } -## ๐Ÿ” Review Skill +## Create -Use `ai/skills/aidd-upskill/SKILL.md` to review an existing skill against the quality criteria in this guide. +Run: `/aidd-upskill create [name]` + +## Review Run: `/aidd-upskill review [path-to-skill]` diff --git a/ai/skills/aidd-upskill/SKILL.md b/ai/skills/aidd-upskill/SKILL.md index 5aa87bfe..71741dcc 100644 --- a/ai/skills/aidd-upskill/SKILL.md +++ b/ai/skills/aidd-upskill/SKILL.md @@ -9,7 +9,7 @@ description: Guide for crafting high-quality AIDD skills. Use when creating, rev Expert skill author. Craft skills that are clear, minimal, and recomposable, giving agents exactly the context they need โ€” nothing more. -**Skill components:** `ai/skills/aidd-sudolang-syntax`, `ai/skills/aidd-rtc` +**Skill components:** `ai/skills/aidd-sudolang-syntax`, `ai/skills/aidd-please` (provides RTC think()) Constraints { Prefer natural language in markdown format diff --git a/ai/skills/aidd-upskill/references/process.md b/ai/skills/aidd-upskill/references/process.md index 343938ac..3211bbd7 100644 --- a/ai/skills/aidd-upskill/references/process.md +++ b/ai/skills/aidd-upskill/references/process.md @@ -6,7 +6,7 @@ createSkill(userRequest) { gatherRequirements |> nameSkill - |> /aidd-rtc --compact + |> think() --compact |> buildPlan |> presentPlan |> draftSkillMd @@ -78,7 +78,7 @@ reviewSkill(target) { |> checkCommandSeparation |> checkReadme |> deduplicate() - |> /aidd-rtc --compact + |> think() --compact |> reportFindings } ``` @@ -88,6 +88,6 @@ reviewSkill(target) { **checkSizeMetrics** โ€” run `validate-skill` and report warnings **checkCommandSeparation** โ€” verify no command mixes thinking and side effects **checkReadme** โ€” verify README.md exists and contains what/why/commands; flag if it contains implementation details or process narratives -**deduplicate()** โ€” find every instance of repeated information across SKILL.md and its references; flag each duplicate and identify where the single source of truth should live; use `/aidd-rtc --compact` to reason about the canonical location -**/aidd-rtc --compact** โ€” synthesize all findings into a holistic judgment before rendering the verdict; independently testable as a pure thinking stage +**deduplicate()** โ€” find every instance of repeated information across SKILL.md and its references; flag each duplicate and identify where the single source of truth should live; use `think() --compact` to reason about the canonical location +**think() --compact** โ€” synthesize all findings into a holistic judgment before rendering the verdict (uses the RTC think() function from `aidd-please`); independently testable as a pure thinking stage **reportFindings** โ€” produce a per-check pass/fail table (one row per check: runFunctionTest, checkRequiredSections, checkSizeMetrics, checkCommandSeparation, checkReadme, deduplicate) with columns for check name, result (โœ…/โš ๏ธ/โŒ), and detail; conclude with an overall verdict From ae21fc4481504f96116eee3ece1df32a35be35d3 Mon Sep 17 00:00:00 2001 From: janhesters Date: Wed, 15 Apr 2026 15:32:33 +0200 Subject: [PATCH 3/4] Address review feedback on /aidd-upskill - Use safe YAML schema in validate-skill.js to prevent arbitrary type instantiation - Add RequiredSections validation (# Title and ## Steps/Process) - Add description frontmatter to command file - Document token estimation as a rough heuristic (chars / 4) - Decouple validateName test assertions from push order (errors.some) - Add SudoLang spec reference to SKILL.md --- ai/commands/aidd-upskill.md | 4 + ai/skills/aidd-upskill/SKILL.md | 2 + .../aidd-upskill/scripts/validate-skill.js | 9 +- .../scripts/validate-skill.test.js | 107 ++++++++++++++---- 4 files changed, 100 insertions(+), 22 deletions(-) diff --git a/ai/commands/aidd-upskill.md b/ai/commands/aidd-upskill.md index 25f4ab0a..06bd6236 100644 --- a/ai/commands/aidd-upskill.md +++ b/ai/commands/aidd-upskill.md @@ -1,3 +1,7 @@ +--- +description: Create and review AIDD skills following the AgentSkills.io specification and SudoLang authoring patterns. +--- + # ๐Ÿ› ๏ธ /aidd-upskill Load and execute the skill at `ai/skills/aidd-upskill/SKILL.md`. diff --git a/ai/skills/aidd-upskill/SKILL.md b/ai/skills/aidd-upskill/SKILL.md index 71741dcc..f609559f 100644 --- a/ai/skills/aidd-upskill/SKILL.md +++ b/ai/skills/aidd-upskill/SKILL.md @@ -11,6 +11,8 @@ Expert skill author. Craft skills that are clear, minimal, and recomposable, giv **Skill components:** `ai/skills/aidd-sudolang-syntax`, `ai/skills/aidd-please` (provides RTC think()) +**SudoLang spec:** https://github.com/paralleldrive/sudolang/blob/main/sudolang.sudo.md โ€” generated skills must follow SudoLang syntax. + Constraints { Prefer natural language in markdown format Use SudoLang interfaces, pattern matching, and /commands for formal specification diff --git a/ai/skills/aidd-upskill/scripts/validate-skill.js b/ai/skills/aidd-upskill/scripts/validate-skill.js index e1c10ddb..8bfd0981 100644 --- a/ai/skills/aidd-upskill/scripts/validate-skill.js +++ b/ai/skills/aidd-upskill/scripts/validate-skill.js @@ -34,6 +34,8 @@ export const validateName = (name, dirName) => { return errors; }; +// Token counts are rough estimates (chars / 4). This is a heuristic, not a +// real tokenizer โ€” actual counts may vary by 30-50% depending on content. export const calculateMetrics = (frontmatter, body) => ({ bodyLines: body.split("\n").length, bodyTokens: Math.ceil(body.length / 4), @@ -84,7 +86,9 @@ export const validateSkillContent = (content, dirName) => { const errors = []; let parsedFrontmatter; try { - parsedFrontmatter = frontmatter ? yaml.load(frontmatter) : {}; + parsedFrontmatter = frontmatter + ? yaml.load(frontmatter, { schema: yaml.DEFAULT_SAFE_SCHEMA }) + : {}; } catch (e) { errors.push(`Invalid YAML in frontmatter: ${e.message}`); const metrics = calculateMetrics(frontmatter, body); @@ -112,6 +116,9 @@ export const validateSkillContent = (content, dirName) => { if (!description) errors.push("Description is required"); else if (description.length > 1024) errors.push("Description must be 1024 characters or fewer"); + if (!/^# .+/m.test(body)) errors.push("Body must contain a top-level heading (# Title)"); + if (!/^## (?:Steps|Process)/m.test(body)) + errors.push("Body must contain a ## Steps or ## Process section"); const metrics = calculateMetrics(frontmatter, body); const { errors: thresholdErrors, warnings } = checkThresholds(metrics); return { errors: [...errors, ...thresholdErrors], metrics, warnings }; diff --git a/ai/skills/aidd-upskill/scripts/validate-skill.test.js b/ai/skills/aidd-upskill/scripts/validate-skill.test.js index a140e111..5b945b1d 100644 --- a/ai/skills/aidd-upskill/scripts/validate-skill.test.js +++ b/ai/skills/aidd-upskill/scripts/validate-skill.test.js @@ -74,6 +74,8 @@ description: A test skill. --- # My Skill +## Process + Body content here.`; const result = parseSkillMd(content); @@ -89,7 +91,7 @@ Body content here.`; given: "SKILL.md content with valid frontmatter", should: "return body without frontmatter", actual: result.body, - expected: "# My Skill\n\nBody content here.", + expected: "# My Skill\n\n## Process\n\nBody content here.", }); }); @@ -114,7 +116,7 @@ Body content here.`; test("CRLF line endings", () => { const content = - "---\r\nname: aidd-my-skill\r\ndescription: A test skill.\r\n---\r\n# My Skill\r\n\r\nBody content here."; + "---\r\nname: aidd-my-skill\r\ndescription: A test skill.\r\n---\r\n# My Skill\r\n\r\n## Process\r\n\r\nBody content here."; const result = parseSkillMd(content); assert({ @@ -128,7 +130,7 @@ Body content here.`; given: "SKILL.md content with CRLF line endings", should: "return body without frontmatter block", actual: result.body, - expected: "# My Skill\r\n\r\nBody content here.", + expected: "# My Skill\r\n\r\n## Process\r\n\r\nBody content here.", }); }); }); @@ -149,8 +151,8 @@ describe("validateName", () => { assert({ given: "a name without the aidd- prefix", should: "return an error requiring the aidd- prefix", - actual: errors[0], - expected: "Name must start with 'aidd-' prefix", + actual: errors.some((e) => e.includes("aidd-")), + expected: true, }); }); @@ -160,15 +162,15 @@ describe("validateName", () => { assert({ given: "a name with uppercase letters", should: "return an error requiring lowercase alphanumeric + hyphens", - actual: errors[0], - expected: "Name must be lowercase alphanumeric + hyphens only", + actual: errors.some((e) => e.includes("lowercase alphanumeric")), + expected: true, }); assert({ given: "a name with uppercase letters", should: "also return an error requiring the aidd- prefix", - actual: errors[1], - expected: "Name must start with 'aidd-' prefix", + actual: errors.some((e) => e.includes("aidd-")), + expected: true, }); }); @@ -178,15 +180,15 @@ describe("validateName", () => { assert({ given: "a name starting with a hyphen", should: "return an error requiring the aidd- prefix", - actual: errors[0], - expected: "Name must start with 'aidd-' prefix", + actual: errors.some((e) => e.includes("aidd-")), + expected: true, }); assert({ given: "a name starting with a hyphen", should: "return an error about leading/trailing hyphen", - actual: errors[1], - expected: "Name must not start or end with hyphen", + actual: errors.some((e) => e.includes("start or end with hyphen")), + expected: true, }); }); @@ -196,15 +198,15 @@ describe("validateName", () => { assert({ given: "a name ending with a hyphen", should: "return an error requiring the aidd- prefix", - actual: errors[0], - expected: "Name must start with 'aidd-' prefix", + actual: errors.some((e) => e.includes("aidd-")), + expected: true, }); assert({ given: "a name ending with a hyphen", should: "return an error about leading/trailing hyphen", - actual: errors[1], - expected: "Name must not start or end with hyphen", + actual: errors.some((e) => e.includes("start or end with hyphen")), + expected: true, }); }); @@ -214,15 +216,15 @@ describe("validateName", () => { assert({ given: "a name with consecutive hyphens", should: "return an error requiring the aidd- prefix", - actual: errors[0], - expected: "Name must start with 'aidd-' prefix", + actual: errors.some((e) => e.includes("aidd-")), + expected: true, }); assert({ given: "a name with consecutive hyphens", should: "return an error about consecutive hyphens", - actual: errors[1], - expected: "Name must not contain consecutive hyphens", + actual: errors.some((e) => e.includes("consecutive hyphens")), + expected: true, }); }); @@ -499,6 +501,8 @@ description: A test skill. --- # My Skill +## Process + Body content here.`; const result = validateSkillContent(content, "aidd-my-skill"); @@ -558,6 +562,8 @@ description: A valid skill description. --- # My Skill +## Process + Body content here.`; const result = validateSkillContent(content, "aidd-my-skill"); @@ -576,6 +582,8 @@ name: aidd-my-skill --- # My Skill +## Process + Body content here.`; const result = validateSkillContent(content, "aidd-my-skill"); @@ -595,6 +603,8 @@ description: "" --- # My Skill +## Process + Body content here.`; const result = validateSkillContent(content, "aidd-my-skill"); @@ -615,6 +625,8 @@ description: ${longDescription} --- # My Skill +## Process + Body content here.`; const result = validateSkillContent(content, "aidd-my-skill"); @@ -637,6 +649,8 @@ description: ${maxDescription} --- # My Skill +## Process + Body content here.`; const result = validateSkillContent(content, "aidd-my-skill"); @@ -658,6 +672,8 @@ description: A test skill. --- # My Skill +## Process + Body content here.`; const result = validateSkillContent(content, "aidd-my-skill"); @@ -678,6 +694,8 @@ description: A test skill. --- # My Skill +## Process + Body content here.`; const result = validateSkillContent(content, "aidd-my-skill"); @@ -698,6 +716,8 @@ description: [broken --- # My Skill +## Process + Body content here.`; let result; @@ -731,6 +751,8 @@ unknown-key: some value --- # My Skill +## Process + Body content here.`; const result = validateSkillContent(content, "aidd-my-skill"); @@ -752,6 +774,8 @@ description: A test skill. --- # My Skill +## Process + Body content here.`; const result = validateSkillContent(content, "aidd-my-skill"); @@ -771,6 +795,8 @@ just a string --- # My Skill +## Process + Body content here.`; let result; @@ -797,6 +823,45 @@ Body content here.`; expected: true, }); }); + + test("missing top-level heading", () => { + const content = `--- +name: aidd-my-skill +description: A test skill. +--- + +## Process + +Body content here.`; + + const result = validateSkillContent(content, "aidd-my-skill"); + + assert({ + given: "a SKILL.md without a top-level heading", + should: "return an error about missing # Title", + actual: result.errors.some((e) => e.includes("top-level heading")), + expected: true, + }); + }); + + test("missing Process/Steps section", () => { + const content = `--- +name: aidd-my-skill +description: A test skill. +--- +# My Skill + +Body content here.`; + + const result = validateSkillContent(content, "aidd-my-skill"); + + assert({ + given: "a SKILL.md without a ## Steps or ## Process section", + should: "return an error about missing section", + actual: result.errors.some((e) => e.includes("## Steps or ## Process")), + expected: true, + }); + }); }); describe("validateFrontmatterKeys", () => { From 22174a6fcffb0738cecbc57e5a578f50e93f5c35 Mon Sep 17 00:00:00 2001 From: janhesters Date: Wed, 15 Apr 2026 15:32:45 +0200 Subject: [PATCH 4/4] Fix lint --- ai/skills/aidd-upskill/scripts/validate-skill.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ai/skills/aidd-upskill/scripts/validate-skill.js b/ai/skills/aidd-upskill/scripts/validate-skill.js index 8bfd0981..5a85e68d 100644 --- a/ai/skills/aidd-upskill/scripts/validate-skill.js +++ b/ai/skills/aidd-upskill/scripts/validate-skill.js @@ -116,7 +116,8 @@ export const validateSkillContent = (content, dirName) => { if (!description) errors.push("Description is required"); else if (description.length > 1024) errors.push("Description must be 1024 characters or fewer"); - if (!/^# .+/m.test(body)) errors.push("Body must contain a top-level heading (# Title)"); + if (!/^# .+/m.test(body)) + errors.push("Body must contain a top-level heading (# Title)"); if (!/^## (?:Steps|Process)/m.test(body)) errors.push("Body must contain a ## Steps or ## Process section"); const metrics = calculateMetrics(frontmatter, body);