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.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/brute/skill.rb', line 49 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.
119 120 121 122 123 124 125 126 127 128 129 130 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 |
# File 'lib/brute/skill.rb', line 119 def self.all(cwd: Dir.pwd, paths: []) skills = {} seen_files = {} add = lambda do |path, source| skill = load(path, source: source) return unless skill real = realpath(path) return if seen_files[real] if (winner = skills[skill.name]) warn "Skill name collision: '#{skill.name}' at #{path} ignored; " \ "already loaded from #{winner.file_path}" return end seen_files[real] = true skills[skill.name] = skill 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.
161 162 163 |
# File 'lib/brute/skill.rb', line 161 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.
74 75 76 77 78 79 80 81 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 |
# File 'lib/brute/skill.rb', line 74 def self.load(path, source: :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 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, ) 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.
67 |
# File 'lib/brute/skill.rb', line 67 def disable_model_invocation? = @disable_model_invocation |
#location ⇒ Object
Back-compat alias (Tools::SkillLoad era).
70 |
# File 'lib/brute/skill.rb', line 70 def location = file_path |