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 +7 -0
- data/bake/agent/skills.rb +78 -0
- data/context/getting-started.md +71 -0
- data/context/index.yaml +10 -0
- data/lib/agent/skills/definition.rb +38 -0
- data/lib/agent/skills/installer.rb +254 -0
- data/lib/agent/skills/registry.rb +119 -0
- data/lib/agent/skills/version.rb +11 -0
- data/lib/agent/skills.rb +17 -0
- data/license.md +22 -0
- data/readme.md +112 -0
- data/releases.md +7 -0
- data/specification.md +101 -0
- metadata +67 -0
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.
|
data/context/index.yaml
ADDED
|
@@ -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
|
data/lib/agent/skills.rb
ADDED
|
@@ -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
|
+
[](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
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: []
|