Class: Brute::Skill

Inherits:
Object
  • Object
show all
Defined in:
lib/brute/skill.rb

Overview

A single skill: metadata plus the address of its SKILL.md on disk.

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...

The object is a value object — it carries the parsed frontmatter, the body, and the file location, nothing else. Modeled on prime-agent's BaseSkill (packages/coding-agent/src/core/skills.ts).

Discovery is class-level and caller-side: Skill.all scans (in order)

  1. /.brute/skills/**/SKILL.md (project-local, :project)
  2. ~/.config/brute/skills/**/SKILL.md (global, :user)
  3. explicit paths: (dirs or .md files, :path)

First found wins on name collisions (with a stderr warning naming winner and loser), and the same file reached twice via symlinks is skipped.

Parsing and validation mirror the Agent Skills specification (https://agentskills.io/specification). A skill whose frontmatter violates a rule is skipped with a stderr warning naming the rule — never raised.

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 disable-model-invocation].freeze
MAX_NAME_LENGTH =
64
MAX_DESCRIPTION_LENGTH =
1024
MAX_COMPATIBILITY_LENGTH =
500

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, description:, file_path:, content: nil, source: :path, license: nil, compatibility: nil, metadata: nil, allowed_tools: nil, disable_model_invocation: false) ⇒ Skill

Returns a new instance of Skill.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/brute/skill.rb', line 57

def initialize(name:, description:, file_path:, content: nil, source: :path,
               license: nil, compatibility: nil, metadata: nil,
               allowed_tools: nil, disable_model_invocation: false)
  @name = name
  @description = description
  @file_path = file_path
  @base_dir = File.dirname(file_path)
  @content = content
  @source = source
  @license = license
  @compatibility = compatibility
  @metadata = 
  @allowed_tools = allowed_tools
  @disable_model_invocation = disable_model_invocation
end

Instance Attribute Details

#allowed_toolsObject (readonly)

Returns the value of attribute allowed_tools.



37
38
39
# File 'lib/brute/skill.rb', line 37

def allowed_tools
  @allowed_tools
end

#base_dirObject (readonly)

Returns the value of attribute base_dir.



37
38
39
# File 'lib/brute/skill.rb', line 37

def base_dir
  @base_dir
end

#compatibilityObject (readonly)

Returns the value of attribute compatibility.



37
38
39
# File 'lib/brute/skill.rb', line 37

def compatibility
  @compatibility
end

#contentObject (readonly)

Returns the value of attribute content.



37
38
39
# File 'lib/brute/skill.rb', line 37

def content
  @content
end

#descriptionObject (readonly)

Returns the value of attribute description.



37
38
39
# File 'lib/brute/skill.rb', line 37

def description
  @description
end

#file_pathObject (readonly)

Returns the value of attribute file_path.



37
38
39
# File 'lib/brute/skill.rb', line 37

def file_path
  @file_path
end

#licenseObject (readonly)

Returns the value of attribute license.



37
38
39
# File 'lib/brute/skill.rb', line 37

def license
  @license
end

#metadataObject (readonly)

Returns the value of attribute metadata.



37
38
39
# File 'lib/brute/skill.rb', line 37

def 
  @metadata
end

#nameObject (readonly)

Returns the value of attribute name.



37
38
39
# File 'lib/brute/skill.rb', line 37

def name
  @name
end

#sourceObject (readonly)

Returns the value of attribute source.



37
38
39
# File 'lib/brute/skill.rb', line 37

def source
  @source
end

Class Method Details

.all(cwd: Dir.pwd, paths: []) ⇒ Object

Scan all skill directories and return an array of Skills, sorted by name.

Precedence is first-found-wins: project-local overrides global overrides explicit paths. Name collisions warn to stderr naming winner and loser; the same file reached via different symlinks is loaded only once.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/brute/skill.rb', line 131

def self.all(cwd: Dir.pwd, paths: [])
  skills = {}
  seen_files = {}

  add = lambda do |path, source|
    skill = load(path, source: source)
    real  = skill && realpath(path)

    if skill && !seen_files[real]
      if (winner = skills[skill.name])
        warn "Skill name collision: '#{skill.name}' at #{path} ignored; " \
             "already loaded from #{winner.file_path}"
      else
        seen_files[real] = true
        skills[skill.name] = skill
      end
    end
  end

  project = File.join(cwd, ".brute", "skills")
  glob(project) { |path| add.call(path, :project) }

  global = File.join(
    Dir.home,
    ".config",
    "brute",
    "skills",
  )
  glob(global) { |path| add.call(path, :user) }

  paths.each do |raw|
    path = File.expand_path(raw.to_s.sub(/\A~(?=\/|\z)/, Dir.home))
    if File.directory?(path)
      glob(path) { |p| add.call(p, :path) }
    elsif File.file?(path) && path.end_with?(".md")
      add.call(path, :path)
    else
      warn "Skill path #{path} is not a directory or markdown file (ignored)"
    end
  end

  skills.values.sort_by(&:name)
end

.get(name, cwd: Dir.pwd, paths: []) ⇒ Object

Get a single skill by name through the same scan as .all.



176
177
178
# File 'lib/brute/skill.rb', line 176

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

.load(path, source: :path) ⇒ Object

Parse and validate a SKILL.md file into a Skill. Returns nil (with a stderr warning) if the file is invalid.



82
83
84
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
122
123
124
# File 'lib/brute/skill.rb', line 82

def self.load(path, source: :path)
  raw = File.read(path)
  frontmatter, content = parse_frontmatter(path, raw)
  if 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).
    unless frontmatter.key?("name")
      frontmatter = { "name" => dir_name }.merge(frontmatter)
    end

    # 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
    unless extra.empty?
      warn "Skill #{path} has unexpected frontmatter fields (ignored): #{extra.sort.join(', ')}"
    end

    errors = validate(frontmatter, dir_name)
    if errors.empty?
      new(
        name:                     frontmatter["name"].to_s.strip,
        description:              frontmatter["description"].to_s.strip,
        file_path:                path,
        content:                  content.to_s.strip,
        source:                   source,
        license:                  frontmatter["license"]&.to_s,
        compatibility:            frontmatter["compatibility"]&.to_s,
        metadata:                 frontmatter["metadata"],
        allowed_tools:            parse_allowed_tools(frontmatter["allowed-tools"]),
        disable_model_invocation: frontmatter["disable-model-invocation"] == true,
      )
    else
      warn "Skipping invalid skill #{path}: #{errors.join('; ')}"
      nil
    end
  end
rescue => e
  warn "Failed to load skill #{path}: #{e.message}"
  nil
end

Instance Method Details

#disable_model_invocation?Boolean

Hidden from the prompt listing (explicit invocation only), but still handed to the agent as an object.

Returns:

  • (Boolean)


75
# File 'lib/brute/skill.rb', line 75

def disable_model_invocation? = @disable_model_invocation

#locationObject

Back-compat alias (Tools::SkillLoad era).



78
# File 'lib/brute/skill.rb', line 78

def location = file_path