agent-skills 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 246bc0cb05c79a7298244fe476e985567455e43c72cd7cce8b19664c2e41f926
4
+ data.tar.gz: 655771187b61554e981b79d68205e55d0ddd84ebf648548d810551d59938e649
5
+ SHA512:
6
+ metadata.gz: 30d4fb083c05513469fd848e2ba33c0138f014fdf925a05ca96aaabfb78f17de80818baf39ca3dae0d94f28ef98feccbb9855a0ef95850799370ec2b193ae0f5
7
+ data.tar.gz: 0a30bf86be55f17b9add3ecfe378504aa8afa8c11f08a7f564f993e60b24c1be53a25307fc12f8b762bb0fa89f6840fa5c592cb8a861f36e73b9a2f9f93f53dd
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Shopify Inc.
5
+ # Copyright, 2025-2026, by Samuel Williams.
6
+
7
+ require_relative "../../lib/agent/skills/installer"
8
+
9
+ include Agent::Skills
10
+
11
+ def initialize(context)
12
+ super(context)
13
+
14
+ @installer = Installer.new(root: context.root)
15
+ end
16
+
17
+ attr :installer
18
+
19
+ # List gems and the skills they provide.
20
+ # @parameter gem [String] Optional gem name to inspect.
21
+ def list(gem: nil)
22
+ if gem
23
+ skills = @installer.list_skills(gem)
24
+ if skills
25
+ puts "Skills provided by gem '#{gem}':"
26
+ skills.each do |skill|
27
+ puts " #{skill.name} - #{skill.description}"
28
+ end
29
+ else
30
+ puts "No skills found for gem '#{gem}'"
31
+ end
32
+ else
33
+ gems = @installer.find_gems_with_skills
34
+ if gems.any?
35
+ puts "Gems with skills available:"
36
+ gems.each do |gem_information|
37
+ names = gem_information[:skills].map(&:name).join(", ")
38
+ puts " #{gem_information[:name]} (#{gem_information[:version]}): #{names}"
39
+ end
40
+ else
41
+ puts "No gems with skills found"
42
+ end
43
+ end
44
+ end
45
+
46
+ # Show the `SKILL.md` content for a provided skill.
47
+ # @parameter gem [String] The provider gem name.
48
+ # @parameter skill [String] The skill name.
49
+ def show(gem:, skill:)
50
+ content = @installer.show_skill(gem, skill)
51
+ if content
52
+ puts content
53
+ else
54
+ puts "Skill '#{skill}' not found in gem '#{gem}'"
55
+ end
56
+ end
57
+
58
+ # Install skills from gems into `.agents/skills`.
59
+ # @parameter gem [String] Optional gem name to install from.
60
+ def install(gem: nil)
61
+ if gem
62
+ installed = @installer.install_gem_skills(gem)
63
+ if installed
64
+ puts "Installed #{installed.length} skills from gem '#{gem}':"
65
+ installed.each {|skill_name| puts " #{skill_name}"}
66
+ else
67
+ puts "No skills found for gem '#{gem}'"
68
+ end
69
+ else
70
+ installed = @installer.install_all_skills
71
+ if installed.any?
72
+ puts "Installed #{installed.length} skills:"
73
+ installed.each {|skill_name| puts " #{skill_name}"}
74
+ else
75
+ puts "No gems with skills found"
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,71 @@
1
+ # Getting Started with Agent Skills
2
+
3
+ This guide explains how to discover, inspect, and install agent skills provided by Ruby gems.
4
+
5
+ ## Installation
6
+
7
+ Add `agent-skills` to the consuming project:
8
+
9
+ ```bash
10
+ $ bundle add agent-skills
11
+ ```
12
+
13
+ List the skills available from installed gems:
14
+
15
+ ```bash
16
+ $ bundle exec bake agent:skills:list
17
+ ```
18
+
19
+ Install them into the project:
20
+
21
+ ```bash
22
+ $ bundle exec bake agent:skills:install
23
+ ```
24
+
25
+ ## Source and Destination Directories
26
+
27
+ Provider gems publish skills beneath `skills/`:
28
+
29
+ ```text
30
+ provider-gem/
31
+ └── skills/
32
+ └── ruby-testing/
33
+ ├── SKILL.md
34
+ ├── references/
35
+ └── scripts/
36
+ ```
37
+
38
+ The consuming project receives them beneath `.agents/skills/`:
39
+
40
+ ```text
41
+ consumer-project/
42
+ └── .agents/
43
+ └── skills/
44
+ ├── ruby-testing/
45
+ └── .agent-skills.yaml
46
+ ```
47
+
48
+ ## Providing a Skill
49
+
50
+ Create a directory beneath your gem's top-level `skills/` directory. Add a `SKILL.md` file with required frontmatter:
51
+
52
+ ```markdown
53
+ ---
54
+ name: ruby-testing
55
+ description: Test Ruby changes with the project's configured test framework.
56
+ ---
57
+
58
+ # Ruby Testing
59
+
60
+ Run the narrowest relevant test first, then run the complete suite.
61
+ ```
62
+
63
+ The `name` must match the containing directory. Include the complete `skills/**/*` tree in the gemspec.
64
+
65
+ ## Installation Ownership
66
+
67
+ `agent-skills` writes `.agents/skills/.agent-skills.yaml` to record which gem owns each installed skill. It will update skills owned by the same gem, but refuses to overwrite unmanaged skills or skills owned by another gem.
68
+
69
+ ## Safety
70
+
71
+ Skills may contain scripts and operational instructions. Inspect skills with `bake agent:skills:list` and `bake agent:skills:show` before installation when evaluating an unfamiliar dependency.
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Install and manage agent skills from Ruby gems.
3
+ metadata:
4
+ documentation_uri: https://socketry.github.io/agent-skills/
5
+ funding_uri: https://github.com/sponsors/ioquatix/
6
+ source_code_uri: https://github.com/socketry/agent-skills.git
7
+ files:
8
+ - path: getting-started.md
9
+ title: Getting Started with Agent Skills
10
+ description: Discover, inspect, and install agent skills provided by Ruby gems.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Shopify Inc.
5
+ # Copyright, 2025-2026, by Samuel Williams.
6
+
7
+ module Agent
8
+ module Skills
9
+ # A validated skill discovered in a provider gem.
10
+ class Definition
11
+ # Initialize a skill definition.
12
+ #
13
+ # @parameter name [String] The declared skill name.
14
+ # @parameter description [String] A short description used for skill discovery.
15
+ # @parameter path [String] The source directory containing `SKILL.md`.
16
+ # @parameter provider_name [String] The source gem name.
17
+ # @parameter provider_version [String] The source gem version.
18
+ def initialize(name:, description:, path:, provider_name:, provider_version:)
19
+ @name = name
20
+ @description = description
21
+ @path = path
22
+ @provider_name = provider_name
23
+ @provider_version = provider_version
24
+ end
25
+
26
+ attr_reader :name
27
+ attr_reader :description
28
+ attr_reader :path
29
+ attr_reader :provider_name
30
+ attr_reader :provider_version
31
+
32
+ # The path to the skill's required instruction file.
33
+ def skill_file
34
+ File.join(@path, "SKILL.md")
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Shopify Inc.
5
+ # Copyright, 2025-2026, by Samuel Williams.
6
+
7
+ require "fileutils"
8
+ require "rubygems"
9
+ require "yaml"
10
+
11
+ require_relative "definition"
12
+ require_relative "registry"
13
+
14
+ module Agent
15
+ module Skills
16
+ # Discovers and installs skills provided by Ruby gems.
17
+ class Installer
18
+ NAME_PATTERN = Registry::NAME_PATTERN
19
+ MAXIMUM_NAME_LENGTH = 100
20
+ MAXIMUM_DESCRIPTION_LENGTH = 500
21
+
22
+ # Base error for skill discovery and installation failures.
23
+ class Error < StandardError
24
+ end
25
+
26
+ # Raised when a provider contains an invalid skill.
27
+ class InvalidSkill < Error
28
+ end
29
+
30
+ # Raised when a skill would overwrite content owned by another source.
31
+ class Conflict < Error
32
+ end
33
+
34
+ # Initialize a skill installer.
35
+ #
36
+ # @parameter root [String] The consuming project root.
37
+ # @parameter specifications [Gem::Specification] The gem specifications to scan.
38
+ def initialize(root: Dir.pwd, specifications: ::Gem::Specification)
39
+ @root = File.expand_path(root)
40
+ @skills_path = File.join(@root, ".agents", "skills")
41
+ @specifications = specifications
42
+ end
43
+
44
+ attr_reader :root
45
+ attr_reader :skills_path
46
+
47
+ # Find installed gems which provide one or more valid skills.
48
+ #
49
+ # @parameter skip_local [Boolean] Whether to skip a gem loaded from the consuming project root.
50
+ def find_gems_with_skills(skip_local: true)
51
+ @specifications.filter_map do |specification|
52
+ next if skip_local && same_path?(specification.full_gem_path, @root)
53
+
54
+ build_gem_information(specification)
55
+ end
56
+ end
57
+
58
+ # Find a named gem which provides skills.
59
+ #
60
+ # @parameter gem_name [String] The gem name to find.
61
+ def find_gem_with_skills(gem_name)
62
+ specification = @specifications.find {|candidate| candidate.name == gem_name}
63
+ return unless specification
64
+
65
+ build_gem_information(specification)
66
+ end
67
+
68
+ # List skills provided by a named gem.
69
+ #
70
+ # @parameter gem_name [String] The gem name.
71
+ def list_skills(gem_name)
72
+ gem = find_gem_with_skills(gem_name)
73
+ gem && gem[:skills]
74
+ end
75
+
76
+ # Read the instruction file for a skill provided by a gem.
77
+ #
78
+ # @parameter gem_name [String] The provider gem name.
79
+ # @parameter skill_name [String] The declared skill name.
80
+ def show_skill(gem_name, skill_name)
81
+ skills = list_skills(gem_name)
82
+ return unless skills
83
+
84
+ definition = skills.find {|skill| skill.name == skill_name}
85
+ File.read(definition.skill_file) if definition
86
+ end
87
+
88
+ # Install all skills provided by a named gem.
89
+ #
90
+ # @parameter gem_name [String] The provider gem name.
91
+ # @returns [Array(String), nil] Installed skill names, or `nil` if the gem provides no skills.
92
+ def install_gem_skills(gem_name)
93
+ gem = find_gem_with_skills(gem_name)
94
+ return unless gem
95
+
96
+ install_gems([gem])
97
+ end
98
+
99
+ # Install skills from every discovered gem.
100
+ #
101
+ # @parameter skip_local [Boolean] Whether to skip a gem loaded from the consuming project root.
102
+ # @returns [Array(String)] Installed skill names.
103
+ def install_all_skills(skip_local: true)
104
+ install_gems(find_gems_with_skills(skip_local: skip_local))
105
+ end
106
+
107
+ private
108
+
109
+ def build_gem_information(specification)
110
+ source_path = File.join(specification.full_gem_path, "skills")
111
+ return unless Dir.exist?(source_path)
112
+
113
+ skills = discover_skills(source_path, specification.name, specification.version.to_s)
114
+ return if skills.empty?
115
+
116
+ {
117
+ name: specification.name,
118
+ version: specification.version.to_s,
119
+ summary: specification.summary,
120
+ metadata: specification.metadata,
121
+ path: source_path,
122
+ skills: skills,
123
+ }
124
+ end
125
+
126
+ def discover_skills(source_path, provider_name, provider_version)
127
+ Dir.children(source_path).sort.filter_map do |entry|
128
+ skill_path = File.join(source_path, entry)
129
+ next unless File.directory?(skill_path)
130
+
131
+ skill_file = File.join(skill_path, "SKILL.md")
132
+ next unless File.file?(skill_file)
133
+
134
+ metadata = load_skill_metadata(skill_file)
135
+ validate_skill_metadata(metadata, entry, skill_file)
136
+
137
+ Definition.new(
138
+ name: metadata["name"],
139
+ description: metadata["description"],
140
+ path: skill_path,
141
+ provider_name: provider_name,
142
+ provider_version: provider_version,
143
+ )
144
+ end
145
+ end
146
+
147
+ def load_skill_metadata(skill_file)
148
+ content = File.read(skill_file)
149
+ match = content.match(/\A---\s*\n(.*?)\n---\s*(?:\n|\z)/m)
150
+
151
+ unless match
152
+ raise InvalidSkill, "Skill must begin with YAML frontmatter: #{skill_file}"
153
+ end
154
+
155
+ metadata = YAML.safe_load(match[1], aliases: false)
156
+ unless metadata.is_a?(Hash)
157
+ raise InvalidSkill, "Skill frontmatter must be a mapping: #{skill_file}"
158
+ end
159
+
160
+ metadata
161
+ rescue Psych::Exception => error
162
+ raise InvalidSkill, "Invalid skill frontmatter in #{skill_file}: #{error.message}"
163
+ end
164
+
165
+ def validate_skill_metadata(metadata, directory_name, skill_file)
166
+ name = metadata["name"]
167
+ description = metadata["description"]
168
+
169
+ unless name.is_a?(String) && name.match?(NAME_PATTERN) && name.length <= MAXIMUM_NAME_LENGTH
170
+ raise InvalidSkill, "Invalid skill name in #{skill_file}: #{name.inspect}"
171
+ end
172
+
173
+ unless name == directory_name
174
+ raise InvalidSkill, "Skill name #{name.inspect} must match its directory #{directory_name.inspect}"
175
+ end
176
+
177
+ unless description.is_a?(String) && !description.strip.empty? && description.length <= MAXIMUM_DESCRIPTION_LENGTH
178
+ raise InvalidSkill, "Invalid skill description in #{skill_file}"
179
+ end
180
+ end
181
+
182
+ def install_gems(gems)
183
+ definitions = gems.flat_map {|gem| gem[:skills]}
184
+ return [] if definitions.empty?
185
+
186
+ validate_unique_skills(definitions)
187
+
188
+ registry = Registry.new(File.join(@skills_path, Registry::FILE_NAME))
189
+ definitions.each {|definition| validate_destination(definition, registry)}
190
+
191
+ stale_skills = gems.flat_map do |gem|
192
+ current_names = gem[:skills].map(&:name)
193
+ registry.skills_for(gem[:name]).keys - current_names
194
+ end
195
+
196
+ definitions.each do |definition|
197
+ registry.claim(definition.name, definition.provider_name, definition.provider_version)
198
+ end
199
+ stale_skills.each {|skill_name| registry.release(skill_name)}
200
+ registry.save
201
+
202
+ definitions.each {|definition| copy_skill(definition)}
203
+ stale_skills.each {|skill_name| remove_skill(skill_name)}
204
+
205
+ definitions.map(&:name)
206
+ end
207
+
208
+ def validate_unique_skills(definitions)
209
+ duplicates = definitions.group_by(&:name).select {|_name, matches| matches.length > 1}
210
+ return if duplicates.empty?
211
+
212
+ details = duplicates.map do |name, matches|
213
+ providers = matches.map(&:provider_name).uniq.join(", ")
214
+ "#{name} (#{providers})"
215
+ end
216
+
217
+ raise Conflict, "Multiple gems provide the same skill: #{details.join('; ')}"
218
+ end
219
+
220
+ def validate_destination(definition, registry)
221
+ destination = File.join(@skills_path, definition.name)
222
+ owner = registry.owner(definition.name)
223
+
224
+ if owner && owner["gem"] != definition.provider_name
225
+ raise Conflict, "Skill #{definition.name.inspect} is owned by gem #{owner['gem'].inspect}"
226
+ end
227
+
228
+ if path_exists?(destination) && !owner
229
+ raise Conflict, "Skill #{definition.name.inspect} already exists and is not managed by agent-skills"
230
+ end
231
+ end
232
+
233
+ def copy_skill(definition)
234
+ FileUtils.mkdir_p(@skills_path)
235
+ destination = File.join(@skills_path, definition.name)
236
+ FileUtils.rm_rf(destination) if path_exists?(destination)
237
+ FileUtils.cp_r(definition.path, destination, preserve: true)
238
+ end
239
+
240
+ def remove_skill(skill_name)
241
+ destination = File.join(@skills_path, skill_name)
242
+ FileUtils.rm_rf(destination) if path_exists?(destination)
243
+ end
244
+
245
+ def path_exists?(path)
246
+ File.exist?(path) || File.symlink?(path)
247
+ end
248
+
249
+ def same_path?(left, right)
250
+ File.expand_path(left) == File.expand_path(right)
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Shopify Inc.
5
+ # Copyright, 2025-2026, by Samuel Williams.
6
+
7
+ require "fileutils"
8
+ require "yaml"
9
+
10
+ module Agent
11
+ module Skills
12
+ # Tracks which installed skills are owned by which gems.
13
+ class Registry
14
+ FILE_NAME = ".agent-skills.yaml"
15
+ FORMAT_VERSION = 1
16
+ NAME_PATTERN = /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/
17
+
18
+ # Raised when the registry cannot be safely interpreted.
19
+ class Invalid < StandardError
20
+ end
21
+
22
+ # Initialize a registry stored at the given path.
23
+ #
24
+ # @parameter path [String] The registry file path.
25
+ def initialize(path)
26
+ @path = path
27
+ @data = load_data
28
+ end
29
+
30
+ attr_reader :path
31
+
32
+ # Get ownership metadata for a skill.
33
+ #
34
+ # @parameter skill_name [String] The installed skill name.
35
+ def owner(skill_name)
36
+ @data["skills"][skill_name]
37
+ end
38
+
39
+ # Get all skills owned by a gem.
40
+ #
41
+ # @parameter gem_name [String] The provider gem name.
42
+ def skills_for(gem_name)
43
+ @data["skills"].select do |_skill_name, owner|
44
+ owner["gem"] == gem_name
45
+ end
46
+ end
47
+
48
+ # Record ownership of an installed skill.
49
+ #
50
+ # @parameter skill_name [String] The installed skill name.
51
+ # @parameter gem_name [String] The provider gem name.
52
+ # @parameter gem_version [String] The provider gem version.
53
+ def claim(skill_name, gem_name, gem_version)
54
+ @data["skills"][skill_name] = {
55
+ "gem" => gem_name,
56
+ "version" => gem_version,
57
+ }
58
+ end
59
+
60
+ # Remove ownership information for a skill.
61
+ #
62
+ # @parameter skill_name [String] The installed skill name.
63
+ def release(skill_name)
64
+ @data["skills"].delete(skill_name)
65
+ end
66
+
67
+ # Persist the registry atomically.
68
+ def save
69
+ FileUtils.mkdir_p(File.dirname(@path))
70
+
71
+ content = {
72
+ "version" => FORMAT_VERSION,
73
+ "skills" => @data["skills"].sort.to_h,
74
+ }.to_yaml
75
+
76
+ temporary_path = "#{@path}.#{Process.pid}.tmp"
77
+ File.write(temporary_path, content)
78
+ File.rename(temporary_path, @path)
79
+ ensure
80
+ FileUtils.rm_f(temporary_path) if temporary_path
81
+ end
82
+
83
+ private
84
+
85
+ def load_data
86
+ return default_data unless File.exist?(@path)
87
+
88
+ data = YAML.safe_load(File.read(@path), aliases: false)
89
+ validate_data(data)
90
+ data
91
+ rescue Psych::Exception => error
92
+ raise Invalid, "Could not load skill registry #{@path}: #{error.message}"
93
+ end
94
+
95
+ def validate_data(data)
96
+ unless data.is_a?(Hash) && data["version"] == FORMAT_VERSION && data["skills"].is_a?(Hash)
97
+ raise Invalid, "Invalid skill registry format: #{@path}"
98
+ end
99
+
100
+ data["skills"].each do |skill_name, owner|
101
+ unless skill_name.is_a?(String) && skill_name.match?(NAME_PATTERN)
102
+ raise Invalid, "Invalid skill name in registry: #{skill_name.inspect}"
103
+ end
104
+
105
+ unless owner.is_a?(Hash) && owner["gem"].is_a?(String) && owner["version"].is_a?(String)
106
+ raise Invalid, "Invalid owner for skill #{skill_name.inspect}"
107
+ end
108
+ end
109
+ end
110
+
111
+ def default_data
112
+ {
113
+ "version" => FORMAT_VERSION,
114
+ "skills" => {},
115
+ }
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Shopify Inc.
5
+ # Copyright, 2025-2026, by Samuel Williams.
6
+
7
+ module Agent
8
+ module Skills
9
+ VERSION = "0.0.1"
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Shopify Inc.
5
+ # Copyright, 2025-2026, by Samuel Williams.
6
+
7
+ require_relative "skill/version"
8
+ require_relative "skill/definition"
9
+ require_relative "skill/registry"
10
+ require_relative "skill/installer"
11
+
12
+ # @namespace
13
+ module Agent
14
+ # Tools for discovering and installing agent skills from Ruby gems.
15
+ module Skills
16
+ end
17
+ end
data/license.md ADDED
@@ -0,0 +1,22 @@
1
+ # MIT License
2
+
3
+ Copyright, 2025, by Shopify Inc.
4
+ Copyright, 2025-2026, by Samuel Williams.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,112 @@
1
+ # Agent Skills
2
+
3
+ Provides tools for discovering and installing agent skills distributed by Ruby gems.
4
+
5
+ [![Development Status](https://github.com/socketry/agent-skills/workflows/Test/badge.svg)](https://github.com/socketry/agent-skills/actions?workflow=Test)
6
+
7
+ ## Overview
8
+
9
+ Ruby gems can provide reusable agent workflows in a top-level `skills/` directory. `agent-skills` discovers those packages and installs them into the consuming project's `.agents/skills/` directory.
10
+
11
+ Installed skills are copied into the project so they can be used independently of the provider gem's installation path.
12
+
13
+ ## Quick Start
14
+
15
+ Add the gem to your project:
16
+
17
+ ``` bash
18
+ $ bundle add agent-skills
19
+ ```
20
+
21
+ List available skills:
22
+
23
+ ``` bash
24
+ $ bundle exec bake agent:skills:list
25
+ ```
26
+
27
+ Install all available skills:
28
+
29
+ ``` bash
30
+ $ bundle exec bake agent:skills:install
31
+ ```
32
+
33
+ Install skills from a specific gem:
34
+
35
+ ``` bash
36
+ $ bundle exec bake agent:skills:install --gem sus
37
+ ```
38
+
39
+ ## Providing Skills in a Gem
40
+
41
+ Create a top-level `skills/` directory containing one directory per skill:
42
+
43
+ ``` text
44
+ your-gem/
45
+ ├── skills/
46
+ │ └── ruby-testing/
47
+ │ ├── SKILL.md
48
+ │ ├── scripts/
49
+ │ ├── references/
50
+ │ └── assets/
51
+ ├── lib/
52
+ └── your-gem.gemspec
53
+ ```
54
+
55
+ Each `SKILL.md` begins with YAML frontmatter:
56
+
57
+ ``` markdown
58
+ ---
59
+ name: ruby-testing
60
+ description: Test Ruby projects using the project's configured test framework.
61
+ ---
62
+
63
+ # Ruby Testing
64
+
65
+ Read the project instructions and run the narrowest relevant tests first.
66
+ ```
67
+
68
+ The declared `name` must match its containing directory. Ensure `skills/**/*` is included in the gem's packaged files.
69
+
70
+ ## Installation Safety
71
+
72
+ The installer records gem-owned skills in `.agents/skills/.agent-skills.yaml`.
73
+
74
+ - Existing project-authored skills are never overwritten.
75
+ - Different gems cannot install the same skill name.
76
+ - A gem can update or remove only skills previously recorded as belonging to that gem.
77
+ - Invalid `SKILL.md` metadata stops installation with an error.
78
+
79
+ Skills may include scripts and operational instructions. Review the skills supplied by dependencies before installing them.
80
+
81
+ The generated `.agents/` directory should be excluded from version control. Run installation during project setup and in CI when skills are required.
82
+
83
+ ## Commands
84
+
85
+ ``` bash
86
+ # List every gem which provides skills:
87
+ bake agent:skills:list
88
+
89
+ # List skills from one gem:
90
+ bake agent:skills:list --gem sus
91
+
92
+ # Show a skill's SKILL.md:
93
+ bake agent:skills:show --gem sus --skill ruby-testing
94
+
95
+ # Install all discovered skills:
96
+ bake agent:skills:install
97
+
98
+ # Install skills from one gem:
99
+ bake agent:skills:install --gem sus
100
+ ```
101
+
102
+ ## Context
103
+
104
+ This gem provides contextual documentation in `context/` for use with [`agent-context`](https://github.com/socketry/agent-context).
105
+
106
+ ## Contributing
107
+
108
+ We welcome contributions. Please add tests for behavioral changes and run the test suite before opening a pull request.
109
+
110
+ ### Developer Certificate of Origin
111
+
112
+ Contributors must comply with the [Developer Certificate of Origin](https://developercertificate.org/).
data/releases.md ADDED
@@ -0,0 +1,7 @@
1
+ # Releases
2
+
3
+ ## v0.0.0
4
+
5
+ - Initial release.
6
+ - Discover and validate skills provided by Ruby gems.
7
+ - Install project-scoped skills with ownership and collision protection.
data/specification.md ADDED
@@ -0,0 +1,101 @@
1
+ # Agent Skills Packaging Specification
2
+
3
+ ## 1. Purpose
4
+
5
+ This specification defines how Ruby gems can distribute reusable agent skills and how consuming projects install those skills.
6
+
7
+ The packaging convention complements the Agent Skills format: it describes transport through Ruby gems, ownership, and installation without redefining the internal semantics of a skill.
8
+
9
+ ## 2. Provider Layout
10
+
11
+ A provider gem MUST place skills in a top-level `skills/` directory. Each immediate child directory represents one skill and MUST contain `SKILL.md`.
12
+
13
+ ```text
14
+ package-root/
15
+ ├── skills/
16
+ │ ├── first-skill/
17
+ │ │ ├── SKILL.md
18
+ │ │ └── references/
19
+ │ └── second-skill/
20
+ │ ├── SKILL.md
21
+ │ └── scripts/
22
+ ├── lib/
23
+ └── package.gemspec
24
+ ```
25
+
26
+ The provider MUST include the complete `skills/` tree in the packaged gem.
27
+
28
+ ## 3. Skill Metadata
29
+
30
+ `SKILL.md` MUST begin with YAML frontmatter containing:
31
+
32
+ - `name`: A lowercase, hyphen-separated identifier.
33
+ - `description`: A concise explanation of when the skill applies.
34
+
35
+ The declared `name` MUST match the name of the skill directory.
36
+
37
+ Additional metadata MAY be present and is preserved as part of the installed skill.
38
+
39
+ ## 4. Consumer Layout
40
+
41
+ Skills are installed into `.agents/skills/` at the consuming project root:
42
+
43
+ ```text
44
+ project-root/
45
+ └── .agents/
46
+ └── skills/
47
+ ├── first-skill/
48
+ ├── second-skill/
49
+ └── .agent-skills.yaml
50
+ ```
51
+
52
+ Skills are installed directly beneath `.agents/skills/` because skill names form a shared project-level namespace.
53
+
54
+ ## 5. Discovery
55
+
56
+ An installer discovers provider gems by:
57
+
58
+ 1. Enumerating installed Ruby gem specifications.
59
+ 2. Finding top-level `skills/` directories.
60
+ 3. Finding immediate child directories containing `SKILL.md`.
61
+ 4. Parsing and validating required frontmatter.
62
+
63
+ Directories without `SKILL.md` are not considered skills.
64
+
65
+ ## 6. Installation
66
+
67
+ Installation MUST copy the entire skill directory and preserve its internal structure.
68
+
69
+ Before modifying the destination, the installer MUST verify that:
70
+
71
+ - No other provider in the same operation supplies the same skill name.
72
+ - An existing destination is recorded as belonging to the same provider.
73
+ - A project-authored or otherwise unmanaged destination will not be overwritten.
74
+
75
+ When a provider is reinstalled, skills formerly owned by that provider but no longer present MAY be removed.
76
+
77
+ ## 7. Ownership Registry
78
+
79
+ The `.agents/skills/.agent-skills.yaml` file records installed skill ownership. Its initial format is:
80
+
81
+ ```yaml
82
+ version: 1
83
+ skills:
84
+ ruby-testing:
85
+ gem: sus
86
+ version: 1.0.0
87
+ ```
88
+
89
+ The registry MUST NOT claim ownership of pre-existing project-authored skills.
90
+
91
+ ## 8. Security
92
+
93
+ Skills can contain scripts and instructions which cause agents to execute commands or modify files. Installation SHOULD therefore be an explicit project action.
94
+
95
+ Consumers SHOULD review provider skills and dependency changes before installation. Installers MUST reject ambiguous ownership rather than selecting a provider implicitly.
96
+
97
+ ## 9. Version Control
98
+
99
+ Projects SHOULD exclude the generated `.agents/` directory from version control and run installation as part of setup and CI when skills are required.
100
+
101
+ Installed skills and the ownership registry MUST contain only reproducible content which can be regenerated from installed packages.
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: agent-skills
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ - Shopify Inc.
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.23'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0.23'
27
+ executables: []
28
+ extensions: []
29
+ extra_rdoc_files: []
30
+ files:
31
+ - bake/agent/skills.rb
32
+ - context/getting-started.md
33
+ - context/index.yaml
34
+ - lib/agent/skills.rb
35
+ - lib/agent/skills/definition.rb
36
+ - lib/agent/skills/installer.rb
37
+ - lib/agent/skills/registry.rb
38
+ - lib/agent/skills/version.rb
39
+ - license.md
40
+ - readme.md
41
+ - releases.md
42
+ - specification.md
43
+ homepage: https://github.com/socketry/agent-skills
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ documentation_uri: https://socketry.github.io/agent-skills/
48
+ funding_uri: https://github.com/sponsors/ioquatix/
49
+ source_code_uri: https://github.com/socketry/agent-skills.git
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '3.2'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 4.0.16
65
+ specification_version: 4
66
+ summary: Install and manage agent skills from Ruby gems.
67
+ test_files: []