Module: Brute::Skill

Defined in:
lib/brute/skill.rb

Overview

Discovers and loads SKILL.md files from standard directories.

A skill is a 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)

The directory name containing SKILL.md becomes the skill name if frontmatter doesn’t specify one.

Defined Under Namespace

Classes: Info

Constant Summary collapse

FILENAME =
"SKILL.md"

Class Method Summary collapse

Class Method Details

.all(cwd: Dir.pwd) ⇒ Object

Scan all skill directories and return an array of Info structs.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/brute/skill.rb', line 30

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.



51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/brute/skill.rb', line 51

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.



46
47
48
# File 'lib/brute/skill.rb', line 46

def self.get(name, cwd: Dir.pwd)
  all(cwd: cwd).detect { |s| s.name == name }
end

.load(path) ⇒ Object

Parse a SKILL.md file into an Info struct. Returns nil if the file is invalid or missing required fields.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/brute/skill.rb', line 67

def self.load(path)
  raw = File.read(path)
  frontmatter, content = parse_frontmatter(raw)
  return nil unless frontmatter

  name = frontmatter["name"] || File.basename(File.dirname(path))
  description = frontmatter["description"]
  return nil unless description && !description.strip.empty?

  Info.new(
    name: name.to_s.strip,
    description: description.to_s.strip,
    location: path,
    content: content.to_s.strip,
  )
rescue => e
  warn "Failed to load skill #{path}: #{e.message}"
  nil
end