-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add validation script and nested AGENTS.md to enforce agent skill quality #53721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c0dac1b
63b0755
7664588
735901b
208de5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Agent Skills | ||
|
|
||
| When creating skills, follow: | ||
| - Agent skills specification: https://agentskills.io/specification.md | ||
| - Best practices: https://agentskills.io/skill-creation/best-practices.md | ||
|
|
||
| ## Structure | ||
|
|
||
| ``` | ||
| .github/skills/skill-name/ | ||
| ├── SKILL.md # Required: metadata + instructions | ||
| ├── scripts/ # Optional: executable code | ||
| ├── references/ # Optional: documentation | ||
| ├── assets/ # Optional: templates, resources | ||
| └── ... # Any additional files or directories | ||
| ``` | ||
|
|
||
| ## Quick Checklist | ||
|
|
||
| - [ ] Run `dotnet .github/skills/ValidateSkill.cs <skill-dir>` to validate format. | ||
| - [ ] `description` describes what the skill does and when to use it. Skill body does not include "When to use this skill". | ||
| - [ ] Skill does not explain things the agent already knows. Focus on what's specific to the task at hand. | ||
| - [ ] Deterministic processes use scripts (for example, to fetch and format data from an API). | ||
| - [ ] Scripts use PowerShell or .NET file-based apps, not bash. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| #!/usr/bin/env dotnet | ||
| #:property ManagePackageVersionsCentrally=false | ||
| #:property PublishAot=false | ||
| #:package YamlDotNet@16.3.0 | ||
|
|
||
| using YamlDotNet.Serialization; | ||
| using System.Text.RegularExpressions; | ||
|
|
||
| if (args.Length == 0) | ||
| { | ||
| Console.Error.WriteLine("Usage: dotnet ValidateSkill.cs <path-to-skill-directory>"); | ||
| return 1; | ||
| } | ||
|
|
||
| string skillDir = Path.GetFullPath(args[0]); | ||
| string skillName = Path.GetFileName(Path.TrimEndingDirectorySeparator(skillDir)); | ||
| string skillFile = Path.Combine(skillDir, "SKILL.md"); | ||
|
|
||
| // SKILL.md must exist in the skill directory | ||
| if (!File.Exists(skillFile)) | ||
| { | ||
| Console.Error.WriteLine($"SKILL.md not found in {skillDir}"); | ||
| return 1; | ||
| } | ||
|
|
||
| string text = File.ReadAllText(skillFile); | ||
|
|
||
| // SKILL.md must begin with YAML frontmatter delimited by --- | ||
| if (!text.StartsWith("---")) | ||
| { | ||
| Console.Error.WriteLine("No YAML frontmatter found."); | ||
| return 1; | ||
| } | ||
|
|
||
| Match frontmatterMatch = Regex.Match( | ||
| text, | ||
| @"\A---\r?\n(?<yaml>.*?)(?:\r?\n)---(?:\r?\n|$)", | ||
| RegexOptions.Singleline); | ||
| if (!frontmatterMatch.Success) | ||
| { | ||
| Console.Error.WriteLine("Unterminated YAML frontmatter."); | ||
| return 1; | ||
| } | ||
|
|
||
| string yaml = frontmatterMatch.Groups["yaml"].Value.Trim(); | ||
|
|
||
| IDeserializer deserializer = new DeserializerBuilder().Build(); | ||
| Dictionary<string, object> frontmatter = deserializer.Deserialize<Dictionary<string, object>>(yaml); | ||
|
|
||
|
Comment on lines
+47
to
+49
|
||
| // name is required | ||
| if (!frontmatter.TryGetValue("name", out object? nameValue) || nameValue is not string frontmatterName) | ||
| { | ||
| Console.Error.WriteLine("Frontmatter missing 'name' field."); | ||
| return 1; | ||
| } | ||
|
|
||
| // name must be 1-64 characters | ||
| if (frontmatterName.Length == 0 || frontmatterName.Length > 64) | ||
| { | ||
| Console.Error.WriteLine($"Name is {frontmatterName.Length} chars (must be 1-64)."); | ||
| return 1; | ||
| } | ||
|
|
||
| // name: lowercase alphanumeric and hyphens only, no leading/trailing/consecutive hyphens | ||
| if (!Regex.IsMatch(frontmatterName, @"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$") | ||
| || frontmatterName.Contains("--")) | ||
| { | ||
| Console.Error.WriteLine($"Invalid name '{frontmatterName}'. Must be lowercase letters, numbers, and hyphens only. Must not start/end with a hyphen or contain consecutive hyphens."); | ||
| return 1; | ||
| } | ||
|
|
||
| // name must match the parent directory name | ||
| if (!string.Equals(skillName, frontmatterName, StringComparison.Ordinal)) | ||
| { | ||
| Console.Error.WriteLine($"Name mismatch: directory is '{skillName}' but SKILL.md name is '{frontmatterName}'."); | ||
| return 1; | ||
| } | ||
|
|
||
| // description is required | ||
| if (!frontmatter.TryGetValue("description", out object? descValue) || descValue is not string description) | ||
| { | ||
| Console.Error.WriteLine("Frontmatter missing 'description' field."); | ||
| return 1; | ||
| } | ||
|
|
||
| // description must be 1-1024 characters | ||
| if (description.Length == 0 || description.Length > 1024) | ||
| { | ||
| Console.Error.WriteLine($"Description is {description.Length} chars (must be 1-1024)."); | ||
| return 1; | ||
| } | ||
|
|
||
| // Keep SKILL.md under 500 lines; move detailed content to references/ or scripts/ | ||
| // See "Progressive Disclosure" at https://agentskills.io/specification.md | ||
| int lineCount = text.Split('\n').Length; | ||
| if (lineCount > 500) | ||
| { | ||
| Console.Error.WriteLine($"SKILL.md is {lineCount} lines (max 500). See \"Progressive Disclosure\" at https://agentskills.io/specification.md"); | ||
| return 1; | ||
|
Comment on lines
+95
to
+99
|
||
| } | ||
|
|
||
| Console.WriteLine($"Skill '{frontmatterName}' is valid."); | ||
| return 0; | ||
Uh oh!
There was an error while loading. Please reload this page.