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

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.

Returns:

  • (Boolean)


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