Class: Brute::Skill
- Inherits:
-
Object
- Object
- Brute::Skill
- 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)
/.brute/skills/**/SKILL.md (project-local, :project) - ~/.config/brute/skills/**/SKILL.md (global, :user)
- 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
-
#allowed_tools ⇒ Object
readonly
Returns the value of attribute allowed_tools.
-
#base_dir ⇒ Object
readonly
Returns the value of attribute base_dir.
-
#compatibility ⇒ Object
readonly
Returns the value of attribute compatibility.
-
#content ⇒ Object
readonly
Returns the value of attribute content.
-
#description ⇒ Object
readonly
Returns the value of attribute description.
-
#file_path ⇒ Object
readonly
Returns the value of attribute file_path.
-
#license ⇒ Object
readonly
Returns the value of attribute license.
-
#metadata ⇒ Object
readonly
Returns the value of attribute metadata.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#source ⇒ Object
readonly
Returns the value of attribute source.
Class Method Summary collapse
-
.all(cwd: Dir.pwd, paths: []) ⇒ Object
Scan all skill directories and return an array of Skills, sorted by name.
-
.get(name, cwd: Dir.pwd, paths: []) ⇒ Object
Get a single skill by name through the same scan as .all.
-
.load(path, source: :path) ⇒ Object
Parse and validate a SKILL.md file into a Skill.
Instance Method Summary collapse
-
#disable_model_invocation? ⇒ Boolean
Hidden from the prompt listing (explicit invocation only), but still handed to the agent as an object.
-
#initialize(name:, description:, file_path:, content: nil, source: :path, license: nil, compatibility: nil, metadata: nil, allowed_tools: nil, disable_model_invocation: false) ⇒ Skill
constructor
A new instance of Skill.
-
#location ⇒ Object
Back-compat alias (Tools::SkillLoad era).
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_tools ⇒ Object (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_dir ⇒ Object (readonly)
Returns the value of attribute base_dir.
37 38 39 |
# File 'lib/brute/skill.rb', line 37 def base_dir @base_dir end |
#compatibility ⇒ Object (readonly)
Returns the value of attribute compatibility.
37 38 39 |
# File 'lib/brute/skill.rb', line 37 def compatibility @compatibility end |
#content ⇒ Object (readonly)
Returns the value of attribute content.
37 38 39 |
# File 'lib/brute/skill.rb', line 37 def content @content end |
#description ⇒ Object (readonly)
Returns the value of attribute description.
37 38 39 |
# File 'lib/brute/skill.rb', line 37 def description @description end |
#file_path ⇒ Object (readonly)
Returns the value of attribute file_path.
37 38 39 |
# File 'lib/brute/skill.rb', line 37 def file_path @file_path end |
#license ⇒ Object (readonly)
Returns the value of attribute license.
37 38 39 |
# File 'lib/brute/skill.rb', line 37 def license @license end |
#metadata ⇒ Object (readonly)
Returns the value of attribute metadata.
37 38 39 |
# File 'lib/brute/skill.rb', line 37 def @metadata end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
37 38 39 |
# File 'lib/brute/skill.rb', line 37 def name @name end |
#source ⇒ Object (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.(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.}" 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.
75 |
# File 'lib/brute/skill.rb', line 75 def disable_model_invocation? = @disable_model_invocation |
#location ⇒ Object
Back-compat alias (Tools::SkillLoad era).
78 |
# File 'lib/brute/skill.rb', line 78 def location = file_path |