Module: Brute::Skill
- Defined in:
- lib/brute/skill.rb
Overview
Discovers, validates, and loads SKILL.md files from standard directories.
A skill is a directory containing a SKILL.md markdown file with YAML frontmatter:
---
name: debugging
description: Systematic debugging workflow for isolating and fixing bugs
---
When debugging, follow these steps...
Skills are scanned from (in order):
1. .brute/skills/**/SKILL.md (project-local)
2. ~/.config/brute/skills/**/SKILL.md (global)
Parsing and validation mirror the Agent Skills specification (https://agentskills.io/specification) and its reference validator (https://github.com/agentskills/agentskills/tree/main/skills-ref). A skill whose frontmatter violates a rule is skipped with a stderr warning naming the violated rule, never raised.
Defined Under Namespace
Classes: Info
Constant Summary collapse
- FILENAME =
"SKILL.md"- ALLOWED_FIELDS =
Frontmatter keys permitted by the spec. Anything else is a violation.
%w[name description license allowed-tools metadata compatibility].freeze
- MAX_NAME_LENGTH =
64- MAX_DESCRIPTION_LENGTH =
1024- MAX_COMPATIBILITY_LENGTH =
500
Class Method Summary collapse
-
.all(cwd: Dir.pwd) ⇒ Object
Scan all skill directories and return an array of Info structs.
-
.fmt(skills) ⇒ Object
Format skills as XML for the system prompt.
-
.get(name, cwd: Dir.pwd) ⇒ Object
Get a single skill by name.
-
.load(path) ⇒ Object
Parse and validate a SKILL.md file into an Info struct.
Class Method Details
.all(cwd: Dir.pwd) ⇒ Object
Scan all skill directories and return an array of Info structs.
48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/brute/skill.rb', line 48 def self.all(cwd: Dir.pwd) skills = {} scan_dirs(cwd).each do |dir| Dir.glob(File.join(dir, "**", FILENAME)).sort.each do |path| info = load(path) next unless info # First found wins (project-local overrides global) skills[info.name] ||= info end end skills.values.sort_by(&:name) end |
.fmt(skills) ⇒ Object
Format skills as XML for the system prompt.
69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/brute/skill.rb', line 69 def self.fmt(skills) return nil if skills.empty? lines = ["<available_skills>"] skills.each do |skill| lines << " <skill>" lines << " <name>#{skill.name}</name>" lines << " <description>#{skill.description}</description>" lines << " </skill>" end lines << "</available_skills>" lines.join("\n") end |
.get(name, cwd: Dir.pwd) ⇒ Object
Get a single skill by name.
64 65 66 |
# File 'lib/brute/skill.rb', line 64 def self.get(name, cwd: Dir.pwd) all(cwd: cwd).detect { |s| s.name == name } end |
.load(path) ⇒ Object
Parse and validate a SKILL.md file into an Info struct. Returns nil (with a stderr warning) if the file is invalid.
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/brute/skill.rb', line 85 def self.load(path) raw = File.read(path) frontmatter, content = parse_frontmatter(path, raw) return nil unless frontmatter dir_name = File.basename(File.dirname(path)) # Spec requires `name`; brute keeps the convenience of defaulting to the # directory name when omitted (which trivially satisfies the dir-match rule). frontmatter = { "name" => dir_name }.merge(frontmatter) unless frontmatter.key?("name") # Unknown fields are a soft violation: warn and drop them rather than # reject the skill. The reference validator hard-fails here, but a runtime # loader must tolerate vendor/forward extensions (e.g. `tags`), which real # published skills carry, instead of silently dropping the whole skill. extra = frontmatter.keys - ALLOWED_FIELDS warn "Skill #{path} has unexpected frontmatter fields (ignored): #{extra.sort.join(', ')}" unless extra.empty? errors = validate(frontmatter, dir_name) unless errors.empty? warn "Skipping invalid skill #{path}: #{errors.join('; ')}" return nil end Info.new( name: frontmatter["name"].to_s.strip, description: frontmatter["description"].to_s.strip, location: path, content: content.to_s.strip, license: frontmatter["license"]&.to_s, compatibility: frontmatter["compatibility"]&.to_s, metadata: frontmatter["metadata"], allowed_tools: parse_allowed_tools(frontmatter["allowed-tools"]), ) rescue => e warn "Failed to load skill #{path}: #{e.}" nil end |