Module: Gem::Skill::Frontmatter
- Defined in:
- lib/gem/skill/frontmatter.rb
Overview
Builds the YAML frontmatter that makes a SKILL.md discoverable as an Agent Skill. Both Claude Code and OpenAI Codex require ‘name` + `description` frontmatter; without it the file is never registered/triggered as a skill.
Constraints satisfied here (intersection of both assistants):
- name: lowercase letters, digits, hyphens only; no leading/trailing or
doubled hyphens; <= 40 chars (Claude Code's rule, also valid for Codex)
- description: single line, no angle brackets (Claude Code rejects < and >),
length-capped (Codex shortens long descriptions)
Generation is deterministic (no LLM): the name is derived from the gem name and the description from the skill’s Overview section, so the frontmatter is always valid regardless of what the model emitted.
Constant Summary collapse
- MAX_NAME_LENGTH =
40- MAX_DESCRIPTION_LENGTH =
500
Class Method Summary collapse
-
.build(gem_name, version, content) ⇒ Object
Return content with a freshly-built, valid frontmatter block.
-
.description_for(gem_name, version, body) ⇒ Object
Derive a trigger-oriented description from the body’s Overview section, appending the version for context.
-
.present?(content) ⇒ Boolean
True when content already begins with a YAML frontmatter block.
-
.slug(gem_name) ⇒ Object
Gem name -> valid skill name.
-
.strip(content) ⇒ Object
Remove a leading frontmatter block (if any) and return the body.
-
.yaml_quote(str) ⇒ Object
Quote a string as a YAML double-quoted scalar, escaping \ and “.
Class Method Details
.build(gem_name, version, content) ⇒ Object
Return content with a freshly-built, valid frontmatter block. Any existing leading frontmatter is stripped and replaced, so this is idempotent.
25 26 27 28 29 |
# File 'lib/gem/skill/frontmatter.rb', line 25 def build(gem_name, version, content) body = strip(content) fm = "---\nname: #{slug(gem_name)}\ndescription: #{yaml_quote(description_for(gem_name, version, body))}\n---\n" "#{fm}\n#{body}" end |
.description_for(gem_name, version, body) ⇒ Object
Derive a trigger-oriented description from the body’s Overview section, appending the version for context. Sanitized for both assistants.
51 52 53 54 55 56 57 |
# File 'lib/gem/skill/frontmatter.rb', line 51 def description_for(gem_name, version, body) overview = body[/^##\s+Overview\s*\n+(.+?)(?=\n\s*\n|\n##\s|\z)/m, 1] text = overview || "Ruby gem #{gem_name}. Use when working with #{gem_name} in Ruby code." text = text.gsub(/\s+/, " ").delete("<>").strip text = "#{text} (#{gem_name} v#{version})" unless text.include?(version.to_s) text[0, MAX_DESCRIPTION_LENGTH].strip end |
.present?(content) ⇒ Boolean
True when content already begins with a YAML frontmatter block.
32 33 34 |
# File 'lib/gem/skill/frontmatter.rb', line 32 def present?(content) content.to_s.lstrip.start_with?("---") end |
.slug(gem_name) ⇒ Object
Gem name -> valid skill name. “ruby_llm” -> “ruby-llm”, “TTY-Spinner” -> “tty-spinner”. Falls back to “skill” if nothing usable remains.
43 44 45 46 47 |
# File 'lib/gem/skill/frontmatter.rb', line 43 def slug(gem_name) s = gem_name.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-+|-+\z/, "") s = "skill" if s.empty? s[0, MAX_NAME_LENGTH].sub(/-+\z/, "") end |
.strip(content) ⇒ Object
Remove a leading frontmatter block (if any) and return the body.
37 38 39 |
# File 'lib/gem/skill/frontmatter.rb', line 37 def strip(content) content.to_s.sub(/\A\s*---\s*\n.*?\n---\s*\n+/m, "").lstrip end |
.yaml_quote(str) ⇒ Object
Quote a string as a YAML double-quoted scalar, escaping \ and “.
60 61 62 |
# File 'lib/gem/skill/frontmatter.rb', line 60 def yaml_quote(str) %("#{str.gsub(/[\\"]/) { |c| "\\#{c}" }}") end |