From 6a03ca7c68a2b81f4fccb2e292eab4e2d9bb0ca6 Mon Sep 17 00:00:00 2001 From: devrimcavusoglu Date: Mon, 13 Apr 2026 03:16:02 +0300 Subject: [PATCH 1/3] Add skill creation guidelines based on writing-skills (#70) Integrate writing-skills guidelines into skern's skill create workflow: - Update DefaultBody() scaffold with recommended sections (Overview, When to Use, Core Pattern, Quick Reference, Common Mistakes) - Update default description to start with "Use when..." (CSO guideline) - Add lint hints for description trigger prefix and missing body sections - Add Writing Skills Guidelines section to AGENTS.md - Add docs/guide/writing-skills.md documentation page - Add comprehensive tests for new lint rules Co-Authored-By: Claude Opus 4.6 (1M context) --- AGENTS.md | 80 +++++++++++++- docs/.vitepress/config.mts | 1 + docs/guide/writing-skills.md | 152 ++++++++++++++++++++++++++ internal/skill/scaffold.go | 42 ++++++- internal/skill/scaffold_test.go | 20 +++- internal/skill/validator.go | 61 ++++++++++- internal/skill/validator_test.go | 182 ++++++++++++++++++++++++++++--- 7 files changed, 512 insertions(+), 26 deletions(-) create mode 100644 docs/guide/writing-skills.md diff --git a/AGENTS.md b/AGENTS.md index e2c8ecd..a9f9571 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -169,7 +169,8 @@ On subsequent encounters, the agent finds the existing skill via `skern skill se --- name: skill-name description: | - What this skill does and when to use it + Use when . +tags: [] allowed-tools: [] metadata: author: @@ -184,13 +185,86 @@ metadata: date: "2025-07-15T10:30:00Z" --- -## Instructions +## Overview -Step-by-step instructions for the agent. +1-2 sentence core principle of the skill. + +## When to Use + +- Triggering conditions and symptoms + +## Core Pattern + +The main technique or pattern (before/after for techniques). + +## Quick Reference + +- Scannable summary for fast lookup + +## Common Mistakes + +- Frequent errors and fixes ``` Required fields: `name`, `description`. Directory name must match the `name` field. +### Writing Skills Guidelines + +When creating or editing skills (via `skern skill create` or manually), follow these guidelines adapted from [superpowers/writing-skills](https://github.com/obra/superpowers/tree/main/skills/writing-skills): + +#### Description Field (Claude Search Optimization) + +- **Start with "Use when..."** — describe the triggering conditions, not a summary of what the skill does +- Agents load skills based on description matching. If the description summarizes the workflow, agents may shortcut and follow the description instead of reading the full SKILL.md +- Include error messages, symptom words ("flaky", "hanging", "race condition"), tool names, and synonyms to improve discoverability + +#### Naming Conventions + +- Use `kebab-case` with verb-first active voice: `creating-skills` not `skill-creation` +- Names must match `^[a-z0-9]+(-[a-z0-9]+)*$` and be 1-64 characters + +#### Recommended Body Structure + +| Section | Purpose | +|---------|---------| +| **Overview** | 1-2 sentence core principle | +| **When to Use** | Symptoms, use cases, when NOT to use | +| **Core Pattern** | Main technique; before/after for techniques | +| **Quick Reference** | Scannable table or bullets | +| **Common Mistakes** | Frequent errors + fixes | + +#### Token Efficiency + +- Getting-started skills: < 150 words +- Frequently-loaded skills: < 200 words +- Other skills: < 500 words +- Use cross-references for shared concepts; compress examples + +#### Skill Types + +| Type | Description | Example | +|------|-------------|---------| +| **Technique** | Concrete method with steps | `condition-based-waiting` | +| **Pattern** | Mental model for problem-solving | `flatten-with-flags` | +| **Reference** | API docs, syntax guides | `office-docs` | + +#### Code Examples + +- One excellent example beats many mediocre ones +- Use the most relevant language for the skill's domain + +#### Anti-Patterns + +- Narrative storytelling ("In session 2025-10-03...") +- Multi-language code examples (dilutes quality) +- Generic semantic labels (helper1, step3) +- Summarizing the workflow in the description field + +#### File Organization + +- **Self-contained**: Single SKILL.md when all content fits +- **With supporting files**: Only for heavy reference (100+ lines) or reusable tools + ### Skill Name Validation Names must match `^[a-z0-9]+(-[a-z0-9]+)*$` and be 1-64 characters. diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 13d134a..994f134 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -40,6 +40,7 @@ export default defineConfig({ { text: 'Installation', link: '/guide/installation' }, { text: 'Quick Start', link: '/guide/quick-start' }, { text: 'Agent Setup', link: '/guide/agent-setup' }, + { text: 'Writing Skills', link: '/guide/writing-skills' }, ], }, ], diff --git a/docs/guide/writing-skills.md b/docs/guide/writing-skills.md new file mode 100644 index 0000000..23d9651 --- /dev/null +++ b/docs/guide/writing-skills.md @@ -0,0 +1,152 @@ +# Writing Effective Skills + +This guide covers how to write high-quality, discoverable, well-structured skills for skern. These guidelines are adapted from the [superpowers writing-skills](https://github.com/obra/superpowers/tree/main/skills/writing-skills) project and are built into skern's scaffolding and validation. + +## Skill Types + +| Type | Description | Example Name | +|------|-------------|--------------| +| **Technique** | Concrete method with actionable steps | `condition-based-waiting` | +| **Pattern** | Mental model for problem-solving | `flatten-with-flags` | +| **Reference** | API docs, syntax guides, lookup tables | `office-docs` | + +## When to Create a Skill + +Create a skill when: + +- The technique was not intuitively obvious +- You would reference it across multiple projects +- The pattern applies broadly beyond one codebase +- Others would benefit from it + +Do **not** create a skill for: + +- One-time solutions or narratives about specific sessions +- Patterns already well-documented in official tooling + +## Description: Claude Search Optimization (CSO) + +The `description` field in your SKILL.md frontmatter is the most critical field for agent discoverability. Agents match skills by description, so how you write it directly affects whether your skill gets loaded. + +**Start with "Use when..."** and describe the triggering conditions: + +```yaml +# Good +description: | + Use when tests hang or flake due to timing-dependent assertions. + Symptoms: intermittent CI failures, setTimeout in test code. + +# Bad — summarizes the workflow (agents may shortcut) +description: | + A skill that teaches agents how to write better async tests + using condition-based waiting patterns. +``` + +**Why this matters:** When a description summarizes the skill's workflow, an agent may follow the description as a shortcut instead of reading the full SKILL.md body. + +**Keyword tips:** + +- Include error messages agents might encounter +- Add symptom words: "flaky", "hanging", "race condition", "timeout" +- Use tool names and common synonyms + +## Naming Conventions + +- Use `kebab-case` with lowercase letters, numbers, and hyphens +- Prefer verb-first active voice: `creating-skills` not `skill-creation` +- Be specific: `condition-based-waiting` not `async-test-helpers` + +Names must match `^[a-z0-9]+(-[a-z0-9]+)*$` and be 1-64 characters. + +## Recommended Body Structure + +When you run `skern skill create`, the scaffold includes these sections by default: + +### Overview + +1-2 sentences describing the core principle or technique. Keep it concise. + +### When to Use + +List triggering conditions as bullet points: + +- Symptoms that signal this skill is needed +- Specific use cases +- When **not** to use this skill (counter-examples help agents avoid false matches) + +### Core Pattern + +The main technique or pattern. For techniques, use a before/after comparison: + +```markdown +## Core Pattern + +**Before (anti-pattern):** +`setTimeout(check, 1000)` — arbitrary delay, still flaky + +**After (correct):** +`waitFor(() => expect(value).toBe(true))` — condition-based, deterministic +``` + +### Quick Reference + +A scannable table or bullet list for fast lookup. Agents should be able to use this section as a cheat sheet without reading the full skill. + +### Common Mistakes + +Frequent errors and their fixes. Helps agents self-correct. + +## Token Efficiency + +Skills are loaded into agent context windows, so brevity matters: + +| Skill Category | Target Word Count | +|----------------|-------------------| +| Getting-started workflows | < 150 words | +| Frequently-loaded skills | < 200 words | +| Other skills | < 500 words | + +**Tips:** + +- Use cross-references instead of repeating shared concepts +- Compress examples to the minimum that demonstrates the point +- Move heavy reference material (100+ lines) to supporting files + +## Code Examples + +- **One excellent example beats many mediocre ones** +- Use the most relevant language for the skill's domain +- Keep examples copy-pasteable and runnable + +## File Organization + +| Layout | When to Use | +|--------|------------| +| Single `SKILL.md` | All content fits in one file | +| `SKILL.md` + supporting files | Heavy reference (100+ lines) or reusable tool code | + +Supporting files live in the same directory as `SKILL.md`. Reference them in the body. + +## Anti-Patterns to Avoid + +- **Narrative storytelling** — "In session 2025-10-03, I encountered..." Skills are reusable references, not journals. +- **Multi-language examples** — Pick one language. Multiple languages dilute quality and waste tokens. +- **Generic labels** — Avoid `helper1`, `step3`, `utils`. Use descriptive names. +- **Workflow summaries in description** — The description field is for triggering conditions only. + +## Validation Hints + +Skern's validator provides hints when your skill deviates from these guidelines: + +- **Description missing trigger prefix** — Hints when the description doesn't start with "Use when", "Use for", "Use to", etc. +- **Missing recommended sections** — Hints when the body is missing "When to Use", "Core Pattern", "Quick Reference", or "Common Mistakes" sections. +- **Body too short** — Hints when the body has fewer than 20 words. +- **Description too vague** — Hints when the description has fewer than 3 words. + +These are non-blocking hints (not errors), designed to guide you toward better skills. + +## Next Steps + +- [Quick Start](/guide/quick-start) — create and install your first skill +- [Skill Format](/concepts/skill-format) — full SKILL.md specification +- [Validation Reference](/reference/validation) — all validation rules diff --git a/internal/skill/scaffold.go b/internal/skill/scaffold.go index 8f94d72..a94922e 100644 --- a/internal/skill/scaffold.go +++ b/internal/skill/scaffold.go @@ -1,8 +1,46 @@ package skill // DefaultBody returns the default body content for a new skill. +// The template follows the writing-skills guidelines: Overview, When to Use, +// Core Pattern, Quick Reference, and Common Mistakes sections. func DefaultBody() string { - return "## Instructions\n\nTODO: Add step-by-step instructions for the agent.\n" + return `## Overview + + + +TODO: Describe the core principle of this skill. + +## When to Use + + + +- TODO: Add triggering conditions + +## Core Pattern + + + +TODO: Describe the core pattern or technique. + +## Quick Reference + + + +- TODO: Add quick reference items + +## Common Mistakes + + + +- TODO: Add common mistakes and how to avoid them +` +} + +// DefaultDescription returns the default placeholder description for a new skill. +// Follows the writing-skills guideline: start with "Use when..." to describe +// triggering conditions, not a workflow summary. +func DefaultDescription() string { + return "Use when TODO: describe the triggering conditions for this skill.\n" } // NewSkill creates a new Skill with sensible defaults. @@ -13,7 +51,7 @@ func NewSkill(name, description, authorName, authorType, authorPlatform string) // NewSkillWithBody creates a new Skill with a custom body. If body is empty, DefaultBody() is used. func NewSkillWithBody(name, description, authorName, authorType, authorPlatform, body string) *Skill { if description == "" { - description = "TODO: Describe what this skill does and when to use it.\n" + description = DefaultDescription() } if body == "" { diff --git a/internal/skill/scaffold_test.go b/internal/skill/scaffold_test.go index ab58799..b5f8593 100644 --- a/internal/skill/scaffold_test.go +++ b/internal/skill/scaffold_test.go @@ -10,9 +10,14 @@ func TestNewSkill_NameOnly(t *testing.T) { s := NewSkill("my-skill", "", "", "", "") assert.Equal(t, "my-skill", s.Name) + assert.Contains(t, s.Description, "Use when") assert.Contains(t, s.Description, "TODO") assert.Equal(t, "0.0.1", s.Metadata.Version) - assert.Contains(t, s.Body, "## Instructions") + assert.Contains(t, s.Body, "## Overview") + assert.Contains(t, s.Body, "## When to Use") + assert.Contains(t, s.Body, "## Core Pattern") + assert.Contains(t, s.Body, "## Quick Reference") + assert.Contains(t, s.Body, "## Common Mistakes") } func TestNewSkill_WithDescription(t *testing.T) { @@ -39,6 +44,17 @@ func TestNewSkill_WithAgentAuthor(t *testing.T) { func TestDefaultBody(t *testing.T) { body := DefaultBody() - assert.Contains(t, body, "## Instructions") + assert.Contains(t, body, "## Overview") + assert.Contains(t, body, "## When to Use") + assert.Contains(t, body, "## Core Pattern") + assert.Contains(t, body, "## Quick Reference") + assert.Contains(t, body, "## Common Mistakes") assert.Contains(t, body, "TODO") } + +func TestDefaultDescription(t *testing.T) { + desc := DefaultDescription() + assert.True(t, len(desc) > 0) + assert.Contains(t, desc, "Use when") + assert.Contains(t, desc, "TODO") +} diff --git a/internal/skill/validator.go b/internal/skill/validator.go index 494bfca..2721831 100644 --- a/internal/skill/validator.go +++ b/internal/skill/validator.go @@ -139,6 +139,28 @@ const ( lintDescMinWords = 3 ) +// descriptionTriggerPrefixes are the recommended prefixes for skill descriptions. +// Descriptions should start with a triggering condition (e.g., "Use when...") to +// optimize agent discovery (Claude Search Optimization). Summaries of what the +// skill does cause agents to follow the description shortcut instead of reading +// the full SKILL.md. +var descriptionTriggerPrefixes = []string{ + "use when", + "use for", + "use to", + "trigger when", + "apply when", +} + +// recommendedBodySections lists the sections recommended by the writing-skills +// guidelines. These improve discoverability, scannability, and quality. +var recommendedBodySections = []string{ + "when to use", + "core pattern", + "quick reference", + "common mistakes", +} + // lintStyle performs stylistic quality checks on a skill. // Issues use SeverityHint to distinguish from structural errors/warnings. func lintStyle(s *Skill) []ValidationIssue { @@ -155,7 +177,8 @@ func lintStyle(s *Skill) []ValidationIssue { } // Description too vague (very short) - descWords := len(strings.Fields(strings.TrimSpace(s.Description))) + descTrimmed := strings.TrimSpace(s.Description) + descWords := len(strings.Fields(descTrimmed)) if descWords > 0 && descWords < lintDescMinWords { issues = append(issues, ValidationIssue{ Field: "description", @@ -164,6 +187,25 @@ func lintStyle(s *Skill) []ValidationIssue { }) } + // Description should start with a triggering condition (CSO guideline) + if descWords >= lintDescMinWords { + descLower := strings.ToLower(descTrimmed) + hasTriggerPrefix := false + for _, prefix := range descriptionTriggerPrefixes { + if strings.HasPrefix(descLower, prefix) { + hasTriggerPrefix = true + break + } + } + if !hasTriggerPrefix { + issues = append(issues, ValidationIssue{ + Field: "description", + Severity: SeverityHint, + Message: "description should start with \"Use when\" to describe triggering conditions, not summarize the workflow", + }) + } + } + // Body lacks step-by-step guidance markers bodyLower := strings.ToLower(s.Body) hasSteps := strings.Contains(bodyLower, "step") || @@ -178,6 +220,23 @@ func lintStyle(s *Skill) []ValidationIssue { }) } + // Body should include recommended sections (writing-skills guideline) + if bodyWords >= lintBodyMinWords { + var missing []string + for _, section := range recommendedBodySections { + if !strings.Contains(bodyLower, section) { + missing = append(missing, section) + } + } + if len(missing) > 0 { + issues = append(issues, ValidationIssue{ + Field: "body", + Severity: SeverityHint, + Message: fmt.Sprintf("body is missing recommended sections: %s", strings.Join(missing, ", ")), + }) + } + } + return issues } diff --git a/internal/skill/validator_test.go b/internal/skill/validator_test.go index e59a6d4..359b84c 100644 --- a/internal/skill/validator_test.go +++ b/internal/skill/validator_test.go @@ -11,8 +11,29 @@ import ( func TestValidate_ValidSkill(t *testing.T) { s := &Skill{ Name: "my-skill", - Description: "A valid skill description", - Body: "## Instructions\n\nThis skill provides step-by-step guidance for completing tasks:\n\n- First, analyze the input carefully and validate the data\n- Then, process the data accordingly and format the results\n- Finally, return the formatted output to the user", + Description: "Use when you need to complete tasks with step-by-step guidance", + Body: `## Overview + +This skill provides step-by-step guidance for completing tasks. + +## When to Use + +- When you need structured task completion + +## Core Pattern + +- First, analyze the input carefully and validate the data +- Then, process the data accordingly and format the results +- Finally, return the formatted output to the user + +## Quick Reference + +- Analyze → Process → Format → Return + +## Common Mistakes + +- Skipping validation of input data +`, Metadata: Metadata{ Author: Author{Name: "alice", Type: "human"}, Version: "1.0.0", @@ -268,15 +289,18 @@ func TestValidationIssue_String(t *testing.T) { func TestLintStyle_BodyTooShort(t *testing.T) { s := &Skill{ Name: "my-skill", - Description: "A valid skill description", + Description: "Use when you need a valid skill description", Body: "## Instructions\n\nDo something useful.", } issues := lintStyle(s) - require.Len(t, issues, 1) - assert.Equal(t, "body", issues[0].Field) - assert.Equal(t, SeverityHint, issues[0].Severity) - assert.Contains(t, issues[0].Message, "words") + hasBodyShortHint := false + for _, i := range issues { + if i.Field == "body" && i.Severity == SeverityHint && strings.Contains(i.Message, "words") { + hasBodyShortHint = true + } + } + assert.True(t, hasBodyShortHint) } func TestLintStyle_DescriptionTooVague(t *testing.T) { @@ -287,33 +311,155 @@ func TestLintStyle_DescriptionTooVague(t *testing.T) { } issues := lintStyle(s) - require.Len(t, issues, 1) - assert.Equal(t, "description", issues[0].Field) - assert.Equal(t, SeverityHint, issues[0].Severity) - assert.Contains(t, issues[0].Message, "word") + hasDescVague := false + for _, i := range issues { + if i.Field == "description" && i.Severity == SeverityHint && strings.Contains(i.Message, "word") { + hasDescVague = true + } + } + assert.True(t, hasDescVague) } func TestLintStyle_BodyLacksSteps(t *testing.T) { s := &Skill{ Name: "my-skill", - Description: "A valid skill description", + Description: "Use when you need to do something specific", Body: strings.Repeat("word ", 25), } issues := lintStyle(s) - require.Len(t, issues, 1) - assert.Equal(t, "body", issues[0].Field) - assert.Equal(t, SeverityHint, issues[0].Severity) - assert.Contains(t, issues[0].Message, "step-by-step") + hasStepHint := false + for _, i := range issues { + if i.Field == "body" && i.Severity == SeverityHint && strings.Contains(i.Message, "step-by-step") { + hasStepHint = true + } + } + assert.True(t, hasStepHint) } func TestLintStyle_NoHints(t *testing.T) { s := &Skill{ Name: "my-skill", - Description: "A valid skill description", - Body: "## Instructions\n\nThis skill provides step-by-step guidance for completing tasks:\n\n- First, analyze the input carefully and validate the data\n- Then, process the data accordingly and format the results\n- Finally, return the formatted output to the user", + Description: "Use when you need step-by-step task guidance", + Body: `## Overview + +This skill provides step-by-step guidance for completing tasks. + +## When to Use + +- When you need structured task completion + +## Core Pattern + +- First, analyze the input carefully and validate the data +- Then, process the data accordingly and format the results + +## Quick Reference + +- Analyze → Process → Format → Return + +## Common Mistakes + +- Skipping validation of input data +`, } issues := lintStyle(s) assert.Empty(t, issues) } + +func TestLintStyle_DescriptionMissingTriggerPrefix(t *testing.T) { + s := &Skill{ + Name: "my-skill", + Description: "A skill that helps with task management", + Body: "## Instructions\n\nSome body content here.", + } + + issues := lintStyle(s) + hasTriggerHint := false + for _, i := range issues { + if i.Field == "description" && i.Severity == SeverityHint && strings.Contains(i.Message, "Use when") { + hasTriggerHint = true + } + } + assert.True(t, hasTriggerHint, "should hint that description should start with 'Use when'") +} + +func TestLintStyle_DescriptionWithTriggerPrefix(t *testing.T) { + tests := []struct { + name string + desc string + }{ + {"use when", "Use when deploying services to production"}, + {"use for", "Use for formatting code according to team standards"}, + {"use to", "Use to validate API responses before processing"}, + {"trigger when", "Trigger when a new pull request is opened"}, + {"apply when", "Apply when refactoring legacy code modules"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Skill{ + Name: "my-skill", + Description: tt.desc, + Body: "Short body.", + } + + issues := lintStyle(s) + for _, i := range issues { + if i.Field == "description" && strings.Contains(i.Message, "Use when") { + t.Errorf("should not hint about trigger prefix for description %q", tt.desc) + } + } + }) + } +} + +func TestLintStyle_BodyMissingRecommendedSections(t *testing.T) { + s := &Skill{ + Name: "my-skill", + Description: "Use when you need to do something specific", + Body: "## Instructions\n\nThis skill provides step-by-step guidance for completing tasks:\n\n- First, analyze the input carefully and validate the data\n- Then, process the data accordingly and format the results\n- Finally, return the formatted output to the user", + } + + issues := lintStyle(s) + hasSectionHint := false + for _, i := range issues { + if i.Field == "body" && i.Severity == SeverityHint && strings.Contains(i.Message, "missing recommended sections") { + hasSectionHint = true + assert.Contains(t, i.Message, "when to use") + assert.Contains(t, i.Message, "common mistakes") + } + } + assert.True(t, hasSectionHint, "should hint about missing recommended sections") +} + +func TestLintStyle_BodyWithAllRecommendedSections(t *testing.T) { + s := &Skill{ + Name: "my-skill", + Description: "Use when you need structured guidance", + Body: `## When to Use + +- Structured tasks + +## Core Pattern + +- Step-by-step approach + +## Quick Reference + +- Key points here + +## Common Mistakes + +- Forgetting validation +`, + } + + issues := lintStyle(s) + for _, i := range issues { + if i.Field == "body" && strings.Contains(i.Message, "missing recommended sections") { + t.Errorf("should not hint about missing sections when all are present: %s", i.Message) + } + } +} From da5cdc816305e4e523767d9691d3c83ea7be5d17 Mon Sep 17 00:00:00 2001 From: devrimcavusoglu Date: Tue, 21 Apr 2026 00:28:29 +0300 Subject: [PATCH 2/3] Refine writing-skills doc location and validator heuristics - Move docs/guide/writing-skills.md to docs/writing-skills.md and trim AGENTS.md to a summary that links to the dedicated doc - Expand trigger-phrase prefixes ("Use for/to", "Trigger when", "Apply when") and add "overview" to recommended body sections - Match recommended sections by markdown heading rather than substring to avoid false positives from inline mentions - Ignore .claude/ agent workspace Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 3 +- AGENTS.md | 60 ++++-------------------------- docs/.vitepress/config.mts | 1 - docs/{guide => }/writing-skills.md | 0 internal/skill/scaffold.go | 6 +-- internal/skill/scaffold_test.go | 2 +- internal/skill/validator.go | 29 ++++++++++++++- internal/skill/validator_test.go | 6 ++- 8 files changed, 45 insertions(+), 62 deletions(-) rename docs/{guide => }/writing-skills.md (100%) diff --git a/.gitignore b/.gitignore index fb5842b..c9e9ace 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,8 @@ dist/ coverage.out coverage.html -# IDE +# IDE / agent workspaces +.claude/ .idea/ .vscode/ *.swp diff --git a/AGENTS.md b/AGENTS.md index a9f9571..222b2d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -170,7 +170,6 @@ On subsequent encounters, the agent finds the existing skill via `skern skill se name: skill-name description: | Use when . -tags: [] allowed-tools: [] metadata: author: @@ -210,60 +209,15 @@ Required fields: `name`, `description`. Directory name must match the `name` fie ### Writing Skills Guidelines -When creating or editing skills (via `skern skill create` or manually), follow these guidelines adapted from [superpowers/writing-skills](https://github.com/obra/superpowers/tree/main/skills/writing-skills): +When creating or editing skills (via `skern skill create` or manually), follow these guidelines adapted from [superpowers/writing-skills](https://github.com/obra/superpowers/tree/main/skills/writing-skills). For full details, see [docs/writing-skills.md](docs/writing-skills.md). -#### Description Field (Claude Search Optimization) +Key points: -- **Start with "Use when..."** — describe the triggering conditions, not a summary of what the skill does -- Agents load skills based on description matching. If the description summarizes the workflow, agents may shortcut and follow the description instead of reading the full SKILL.md -- Include error messages, symptom words ("flaky", "hanging", "race condition"), tool names, and synonyms to improve discoverability - -#### Naming Conventions - -- Use `kebab-case` with verb-first active voice: `creating-skills` not `skill-creation` -- Names must match `^[a-z0-9]+(-[a-z0-9]+)*$` and be 1-64 characters - -#### Recommended Body Structure - -| Section | Purpose | -|---------|---------| -| **Overview** | 1-2 sentence core principle | -| **When to Use** | Symptoms, use cases, when NOT to use | -| **Core Pattern** | Main technique; before/after for techniques | -| **Quick Reference** | Scannable table or bullets | -| **Common Mistakes** | Frequent errors + fixes | - -#### Token Efficiency - -- Getting-started skills: < 150 words -- Frequently-loaded skills: < 200 words -- Other skills: < 500 words -- Use cross-references for shared concepts; compress examples - -#### Skill Types - -| Type | Description | Example | -|------|-------------|---------| -| **Technique** | Concrete method with steps | `condition-based-waiting` | -| **Pattern** | Mental model for problem-solving | `flatten-with-flags` | -| **Reference** | API docs, syntax guides | `office-docs` | - -#### Code Examples - -- One excellent example beats many mediocre ones -- Use the most relevant language for the skill's domain - -#### Anti-Patterns - -- Narrative storytelling ("In session 2025-10-03...") -- Multi-language code examples (dilutes quality) -- Generic semantic labels (helper1, step3) -- Summarizing the workflow in the description field - -#### File Organization - -- **Self-contained**: Single SKILL.md when all content fits -- **With supporting files**: Only for heavy reference (100+ lines) or reusable tools +- **Description**: Start with "Use when..." — describe triggering conditions, not a workflow summary +- **Naming**: `kebab-case`, verb-first active voice (`creating-skills` not `skill-creation`) +- **Body structure**: Overview → When to Use → Core Pattern → Quick Reference → Common Mistakes +- **Token budget**: Getting-started < 150 words; frequently-loaded < 200 words; others < 500 words +- **Examples**: One excellent example beats many mediocre ones ### Skill Name Validation diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 994f134..13d134a 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -40,7 +40,6 @@ export default defineConfig({ { text: 'Installation', link: '/guide/installation' }, { text: 'Quick Start', link: '/guide/quick-start' }, { text: 'Agent Setup', link: '/guide/agent-setup' }, - { text: 'Writing Skills', link: '/guide/writing-skills' }, ], }, ], diff --git a/docs/guide/writing-skills.md b/docs/writing-skills.md similarity index 100% rename from docs/guide/writing-skills.md rename to docs/writing-skills.md diff --git a/internal/skill/scaffold.go b/internal/skill/scaffold.go index a94922e..513b289 100644 --- a/internal/skill/scaffold.go +++ b/internal/skill/scaffold.go @@ -36,10 +36,10 @@ TODO: Describe the core pattern or technique. ` } -// DefaultDescription returns the default placeholder description for a new skill. +// defaultDescription returns the default placeholder description for a new skill. // Follows the writing-skills guideline: start with "Use when..." to describe // triggering conditions, not a workflow summary. -func DefaultDescription() string { +func defaultDescription() string { return "Use when TODO: describe the triggering conditions for this skill.\n" } @@ -51,7 +51,7 @@ func NewSkill(name, description, authorName, authorType, authorPlatform string) // NewSkillWithBody creates a new Skill with a custom body. If body is empty, DefaultBody() is used. func NewSkillWithBody(name, description, authorName, authorType, authorPlatform, body string) *Skill { if description == "" { - description = DefaultDescription() + description = defaultDescription() } if body == "" { diff --git a/internal/skill/scaffold_test.go b/internal/skill/scaffold_test.go index b5f8593..a67c743 100644 --- a/internal/skill/scaffold_test.go +++ b/internal/skill/scaffold_test.go @@ -53,7 +53,7 @@ func TestDefaultBody(t *testing.T) { } func TestDefaultDescription(t *testing.T) { - desc := DefaultDescription() + desc := defaultDescription() assert.True(t, len(desc) > 0) assert.Contains(t, desc, "Use when") assert.Contains(t, desc, "TODO") diff --git a/internal/skill/validator.go b/internal/skill/validator.go index 2721831..c7fecae 100644 --- a/internal/skill/validator.go +++ b/internal/skill/validator.go @@ -6,6 +6,7 @@ import ( "path/filepath" "regexp" "strings" + "unicode" ) // Severity represents the severity of a validation issue. @@ -155,12 +156,36 @@ var descriptionTriggerPrefixes = []string{ // recommendedBodySections lists the sections recommended by the writing-skills // guidelines. These improve discoverability, scannability, and quality. var recommendedBodySections = []string{ + "overview", "when to use", "core pattern", "quick reference", "common mistakes", } +// hasMarkdownHeading checks whether the body contains a markdown heading (any level) +// whose text matches the given section name (case-insensitive). +func hasMarkdownHeading(body, section string) bool { + sectionLower := strings.ToLower(section) + for _, line := range strings.Split(body, "\n") { + trimmed := strings.TrimSpace(line) + // Strip leading '#' characters + stripped := strings.TrimLeft(trimmed, "#") + if stripped == trimmed { + continue // no '#' prefix — not a heading + } + // Must have at least one space after '#'s + if len(stripped) == 0 || !unicode.IsSpace(rune(stripped[0])) { + continue + } + headingText := strings.TrimSpace(stripped) + if strings.ToLower(headingText) == sectionLower { + return true + } + } + return false +} + // lintStyle performs stylistic quality checks on a skill. // Issues use SeverityHint to distinguish from structural errors/warnings. func lintStyle(s *Skill) []ValidationIssue { @@ -201,7 +226,7 @@ func lintStyle(s *Skill) []ValidationIssue { issues = append(issues, ValidationIssue{ Field: "description", Severity: SeverityHint, - Message: "description should start with \"Use when\" to describe triggering conditions, not summarize the workflow", + Message: "description should start with a trigger phrase (\"Use when\", \"Use for\", \"Use to\", \"Trigger when\", or \"Apply when\") to describe triggering conditions, not summarize the workflow", }) } } @@ -224,7 +249,7 @@ func lintStyle(s *Skill) []ValidationIssue { if bodyWords >= lintBodyMinWords { var missing []string for _, section := range recommendedBodySections { - if !strings.Contains(bodyLower, section) { + if !hasMarkdownHeading(s.Body, section) { missing = append(missing, section) } } diff --git a/internal/skill/validator_test.go b/internal/skill/validator_test.go index 359b84c..9edfd21 100644 --- a/internal/skill/validator_test.go +++ b/internal/skill/validator_test.go @@ -438,7 +438,11 @@ func TestLintStyle_BodyWithAllRecommendedSections(t *testing.T) { s := &Skill{ Name: "my-skill", Description: "Use when you need structured guidance", - Body: `## When to Use + Body: `## Overview + +- What this skill does + +## When to Use - Structured tasks From 86e721632486c43e06ca5cc88a407e6cc5b7c304 Mon Sep 17 00:00:00 2001 From: devrimcavusoglu Date: Sun, 26 Apr 2026 21:03:59 +0300 Subject: [PATCH 3/3] Test that fresh scaffold produces zero lint hints Guard the central UX promise of the writing-skills guidelines integration: NewSkill + DefaultBody + defaultDescription must together pass lintStyle with zero hints. If any of those drift out of sync with a future lint-rule addition, this test fails. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/skill/validator_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/skill/validator_test.go b/internal/skill/validator_test.go index 9edfd21..fc0cb29 100644 --- a/internal/skill/validator_test.go +++ b/internal/skill/validator_test.go @@ -467,3 +467,15 @@ func TestLintStyle_BodyWithAllRecommendedSections(t *testing.T) { } } } + +// TestScaffold_FreshSkillProducesNoLintHints guards the central UX promise of +// the writing-skills guidelines integration: a freshly scaffolded skill must +// be guideline-compliant out of the box. If DefaultBody, defaultDescription, +// or any lintStyle rule changes such that the scaffold is no longer +// compliant, this test fails and forces them back into sync. +func TestScaffold_FreshSkillProducesNoLintHints(t *testing.T) { + s := NewSkill("my-skill", "", "alice", "human", "") + + issues := lintStyle(s) + assert.Empty(t, issues, "freshly scaffolded skill must produce zero lint hints") +}