ask-skills 0.1.0

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: 1a4229cd07f55413b7de34a7463c2f582aac94898d804e8017cb9c0c02f2117b
4
+ data.tar.gz: d5c96783d0bbd7b5a9a79af85a1c33bdc59f5ffa41f9a612ff24fd61ccea37ef
5
+ SHA512:
6
+ metadata.gz: 10c8342db4297f6d9417be6b63beb7962cd93a0476b421e15c736660a51ccde2c93141ecf6740f53755be02c0371c914ccf1a617759037964810dca38ab37c42
7
+ data.tar.gz: b5f26a5d36086ab1b918375672215fc9e7875ad5a58d43e149a4f744bf4841478bf2bdc1e20a29f9bd8d5f32769680605fe5fd4181cea1cd7b9e4e7d46c5b40b
data/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-06-10
4
+
5
+ ### Added
6
+ - Initial release
7
+ - `Ask::Skills::Skill` — Data.define with name, description, instructions, source
8
+ - `to_s` and `to_prompt_entry` formatting methods
9
+ - `Ask::Skills::Registry` — holds discovered skills with:
10
+ - `[]` lookup, `names` list, `format_for_prompt` markdown output
11
+ - Priority resolution: first source wins
12
+ - `Ask::Skills::Formatter` — generates markdown and XML output
13
+ - `Ask::Skills::Validator` — validates required fields, name format, duplicates
14
+ - `Ask::Skills.discover` — discover skills from all configured sources:
15
+ 1. Project-local (`.agents/skills/`) — highest priority
16
+ 2. User-global (`~/.config/ask/skills/`)
17
+ 3. Installed gems (via `Gem.find_files`)
18
+ 4. Built-in (skill.design, skill.compose) — lowest priority
19
+ - `Ask::Skills::Source::Filesystem` — scan a directory for `*/SKILL.md` files
20
+ - `Ask::Skills::Source::Gems` — discover skills from all installed gems
21
+ - Built-in skills:
22
+ - `skill.design` — How to design and write effective skills
23
+ - `skill.compose` — How skills interact, combine, and resolve
24
+ - Full test suite with 68 tests
25
+ - Frontmatter parsing (name, description, optional metadata)
26
+ - YAML frontmatter support with quoted value handling
27
+ - Error handling for missing directories, malformed skills
28
+ - Thread-safe read-only registry access
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kaka Ruto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # ask-skills
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/ask-skills.svg)](https://badge.fury.io/rb/ask-skills)
4
+
5
+ Discover, validate, and load agent skills from project directories, user config,
6
+ and installed gems. Ships built-in skills for codebase exploration and debugging
7
+ methodology.
8
+
9
+ A **skill** is a markdown file containing step-by-step methodology for a specific
10
+ domain task. It's listed in the agent's system prompt (just name + description)
11
+ and loaded on-demand when the agent decides it needs domain guidance.
12
+
13
+ ## Installation
14
+
15
+ ```ruby
16
+ gem "ask-skills"
17
+ ```
18
+
19
+ Then:
20
+
21
+ ```ruby
22
+ require "ask/skills"
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```ruby
28
+ # Discover all available skills
29
+ registry = Ask::Skills.discover
30
+ # => Finds skills from:
31
+ # - Built-in skills (skill.design, skill.compose)
32
+ # - Installed gems (ask-rails, ask-github, etc.)
33
+ # - .agents/skills/*/ in the project
34
+ # - ~/.config/ask/skills/*/ in home dir
35
+
36
+ # List available skills
37
+ registry.names
38
+ # => ["skill.compose", "skill.design"]
39
+
40
+ # Get a skill by name
41
+ skill = registry["skill.design"]
42
+ skill.name # => "skill.design"
43
+ skill.description # => "How to design and write effective skills for the ask-rb ecosystem"
44
+ skill.instructions # => markdown body with step-by-step methodology
45
+
46
+ # Format for system prompt
47
+ registry.format_for_prompt
48
+ # => "## Available Skills\n\n- **skill.design**: How to design..."
49
+
50
+ # XML format for machine parsing
51
+ formatter = Ask::Skills::Formatter.new(registry)
52
+ formatter.to_xml
53
+ # => "<available_skills><skill><name>skill.design</name>..."
54
+
55
+ # Validate skills
56
+ errors = Ask::Skills::Validator.new(registry.skills.values).validate_all
57
+ ```
58
+
59
+ ## Priority Resolution
60
+
61
+ When the same skill name exists in multiple places, priority determines which
62
+ one is used. **First source wins:**
63
+
64
+ | Priority | Source | Location |
65
+ |----------|--------|----------|
66
+ | 1 (highest) | Project-local | `.agents/skills/<name>/SKILL.md` |
67
+ | 2 | User-global | `~/.config/ask/skills/<name>/SKILL.md` |
68
+ | 3 | Installed gems | `Gem.find_files("ask/skills/*/SKILL.md")` |
69
+ | 4 (lowest) | Built-in | Shipped with ask-skills gem |
70
+
71
+ This means you can override any skill by placing a file with the same name in
72
+ your project's `.agents/skills/` directory.
73
+
74
+ ## Skill Directory Convention
75
+
76
+ ```
77
+ .agents/skills/
78
+ ├── db_debug/
79
+ │ └── SKILL.md ← project-local skill
80
+ ├── deploy/
81
+ │ └── SKILL.md
82
+ └── custom_check/
83
+ └── SKILL.md
84
+
85
+ ~/.config/ask/skills/
86
+ ├── my_workflow/
87
+ │ └── SKILL.md ← user-global skill
88
+ └── team_patterns/
89
+ └── SKILL.md
90
+
91
+ # From installed gems:
92
+ ask-rails-0.2.0/lib/ask/skills/
93
+ ├── rails.db_debug/SKILL.md
94
+ └── rails.deploy_pipeline/SKILL.md
95
+
96
+ ask-github-0.1.0/lib/ask/skills/
97
+ ├── github.pr_review/SKILL.md
98
+ └── github.issue_triage/SKILL.md
99
+ ```
100
+
101
+ ## Skill Format
102
+
103
+ ```markdown
104
+ ---
105
+ name: rails.db_debug
106
+ description: Step-by-step methodology for debugging database issues in Rails
107
+ ---
108
+
109
+ When investigating database performance issues, follow these steps:
110
+
111
+ 1. **Understand the Schema** — Use ReadModel to inspect...
112
+ 2. **Check Indexes** — Query pg_indexes for missing indexes...
113
+ 3. **Explain Slow Queries** — Use EXPLAIN ANALYZE on...
114
+ ```
115
+
116
+ ## Built-in Skills
117
+
118
+ | Skill | Description |
119
+ |-------|-------------|
120
+ | `skill.design` | How to design and write effective skills for the ask-rb ecosystem |
121
+ | `skill.compose` | How skills interact, combine, and resolve in the ask-rb ecosystem |
122
+
123
+ ## API Reference
124
+
125
+ ### `Ask::Skills.discover(sources: nil)`
126
+
127
+ Returns a `Registry` with skills from all sources, in priority order.
128
+ Pass `sources:` to override with custom sources.
129
+
130
+ ### `Ask::Skills::Registry`
131
+
132
+ | Method | Description |
133
+ |--------|-------------|
134
+ | `[]` | Lookup skill by name |
135
+ | `names` | List all skill names |
136
+ | `skills` | Hash of name → Skill |
137
+ | `format_for_prompt` | Generate markdown section |
138
+
139
+ ### `Ask::Skills::Skill` (Data.define)
140
+
141
+ | Attribute | Description |
142
+ |-----------|-------------|
143
+ | `name` | Unique identifier (e.g. `rails.db_debug`) |
144
+ | `description` | One-line summary for system prompt |
145
+ | `instructions` | Full markdown methodology body |
146
+ | `source` | File path the skill was loaded from |
147
+
148
+ ### `Ask::Skills::Formatter`
149
+
150
+ | Method | Description |
151
+ |--------|-------------|
152
+ | `to_prompt_section` | Markdown format for system prompt |
153
+ | `to_xml` | XML format for machine parsing |
154
+
155
+ ### `Ask::Skills::Validator`
156
+
157
+ | Method | Description |
158
+ |--------|-------------|
159
+ | `validate_all` | Validate all skills, return errors |
160
+ | `validate` | Validate a single skill |
161
+
162
+ ## Custom Sources
163
+
164
+ ```ruby
165
+ require "ask/skills"
166
+
167
+ # Custom filesystem source
168
+ custom = Ask::Skills::Source::Filesystem.new(dir: "/path/to/skills")
169
+ registry = Ask::Skills.discover(sources: [custom])
170
+ ```
171
+
172
+ ## Gems That Ship Skills
173
+
174
+ | Gem | Planned Skills |
175
+ |---|---|
176
+ | `ask-skills` (built-in) | skill.design, skill.compose |
177
+ | `ask-rails` | rails.db_debug, rails.route_trouble, rails.deploy_pipeline |
178
+ | `ask-github` | github.pr_review, github.issue_triage |
179
+ | `ask-slack` | slack.compose |
180
+ | `ask-tools-shell` | shell.patterns |
181
+ | `ask-llm-providers` | providers.model_select |
182
+
183
+ ## Development
184
+
185
+ ```bash
186
+ bundle install
187
+ bundle exec rake test
188
+ ```
189
+
190
+ ## License
191
+
192
+ MIT
@@ -0,0 +1,43 @@
1
+ module Ask
2
+ module Skills
3
+ class Formatter
4
+ def initialize(skills)
5
+ @skills = skills.respond_to?(:skills) ? skills.skills : skills
6
+ end
7
+
8
+ def to_prompt_section
9
+ return "" if @skills.empty? || @skills.values.all? { |s| s.is_a?(Array) && s.empty? }
10
+
11
+ lines = ["", "## Available Skills", ""]
12
+ @skills.each_value do |skill|
13
+ lines << skill.to_prompt_entry
14
+ end
15
+ lines << ""
16
+ lines << "When a task matches a skill's description, load it for step-by-step methodology."
17
+ lines << ""
18
+ lines.join("\n")
19
+ end
20
+
21
+ def to_xml
22
+ return "" if @skills.empty?
23
+
24
+ lines = ["", "<available_skills>"]
25
+ @skills.each_value do |skill|
26
+ lines << " <skill>"
27
+ lines << " <name>#{escape_xml(skill.name)}</name>"
28
+ lines << " <description>#{escape_xml(skill.description)}</description>"
29
+ lines << " </skill>"
30
+ end
31
+ lines << "</available_skills>"
32
+ lines.join("\n")
33
+ end
34
+
35
+ private
36
+
37
+ def escape_xml(str)
38
+ str.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
39
+ .gsub('"', "&quot;").gsub("'", "&apos;")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ module Ask
2
+ module Skills
3
+ class Registry
4
+ attr_reader :skills
5
+
6
+ def initialize(sources)
7
+ @skills = {}
8
+ @sources = sources
9
+ load_all
10
+ end
11
+
12
+ def [](name)
13
+ @skills[name]
14
+ end
15
+
16
+ def names
17
+ @skills.keys
18
+ end
19
+
20
+ def format_for_prompt
21
+ return "" if @skills.empty?
22
+ lines = ["", "## Available Skills", ""]
23
+ @skills.each_value do |skill|
24
+ lines << skill.to_prompt_entry
25
+ end
26
+ lines << ""
27
+ lines.join("\n")
28
+ end
29
+
30
+ private
31
+
32
+ def load_all
33
+ @sources.each do |source|
34
+ source.load.each do |skill|
35
+ next unless skill
36
+ # First source wins (project overrides gems, user overrides project)
37
+ @skills[skill.name] ||= skill
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: skill.compose
3
+ description: How skills interact, combine, and resolve in the ask-rb ecosystem
4
+ ---
5
+
6
+ ## Skill Resolution Priority
7
+
8
+ Skills are discovered from multiple sources. When the same skill name exists in
9
+ multiple places, the first source wins:
10
+
11
+ 1. **Built-in** (shipped with ask-skills gem) — lowest priority
12
+ 2. **Installed gems** (ask-rails, ask-github, etc.)
13
+ 3. **User-global** (`~/.config/ask/skills/`)
14
+ 4. **Project-local** (`.agents/skills/` in the project) — highest priority
15
+
16
+ This means you can override any skill by placing a file with the same name in
17
+ your project's `.agents/skills/` directory. Or provide personal defaults in
18
+ `~/.config/ask/skills/`.
19
+
20
+ ## How Skills Appear in the System Prompt
21
+
22
+ The agent's system prompt includes a section listing all available skills:
23
+
24
+ ```
25
+ ## Available Skills
26
+
27
+ - **skill.design**: How to design and write effective skills
28
+ - **rails.db_debug**: Step-by-step database debugging in Rails
29
+ - **github.pr_review**: PR review workflow methodology
30
+ ```
31
+
32
+ Only the **name and description** appear. The full instructions are NOT in the
33
+ system prompt — they're loaded on-demand when the agent calls the skill.
34
+
35
+ ## When to Load a Skill
36
+
37
+ Skills should be loaded when the task matches the skill's description. For example,
38
+ if an investigation involves a slow database query, load `rails.db_debug`.
39
+
40
+ If no skill's description matches the current task, proceed without loading any.
41
+ Skills are optional — they provide methodology, not required capabilities.
42
+
43
+ ## Skills Can Reference Other Skills
44
+
45
+ A skill's instructions can mention other skills:
46
+
47
+ ```markdown
48
+ Step 3: If you find the query is slow, load `rails.db_debug` for
49
+ detailed database investigation methodology.
50
+ ```
51
+
52
+ This creates a hierarchy of methodology — a deploy skill might reference both
53
+ a database migration skill and a monitoring skill.
54
+
55
+ ## Common Patterns
56
+
57
+ **Sequential composition**: Load one skill, follow its steps, then load another
58
+ when you reach a step that references it.
59
+
60
+ **Alternative composition**: If a skill's first step fails, load the troubleshooting
61
+ variant of that skill instead.
62
+
63
+ **Reporting back**: After completing a skill's methodology, summarize what you found
64
+ and what you did. The skill guided your process, but the results go back to the user.
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: skill.design
3
+ description: How to design and write effective skills for the ask-rb ecosystem
4
+ ---
5
+
6
+ A skill is a markdown file that teaches step-by-step methodology for a domain task.
7
+ It differs from a tool (which provides a capability) in that it guides *how* to
8
+ approach the work — the reasoning process, the order of operations, the pitfalls to avoid.
9
+
10
+ ## When to Create a Skill
11
+
12
+ Create a skill when you have domain knowledge that:
13
+ 1. Follows a repeatable process (always debug DB this way)
14
+ 2. Requires tool composition (use these 3 tools in sequence)
15
+ 3. Is too long for the system prompt but too valuable to leave out
16
+
17
+ Do NOT create a skill for:
18
+ - Simple tool knowledge (just update the system prompt or tool description)
19
+ - API reference documentation (that's what context.rb is for in service gems)
20
+ - One-time tasks that won't repeat
21
+
22
+ ## Skill File Format
23
+
24
+ Each skill is a directory with a `SKILL.md` file:
25
+
26
+ ```
27
+ lib/ask/skills/<name>/
28
+ └── SKILL.md
29
+ ```
30
+
31
+ The `SKILL.md` has YAML frontmatter followed by markdown body:
32
+
33
+ ```markdown
34
+ ---
35
+ name: domain.skill_name # Unique identifier (lowercase, dots allowed)
36
+ description: One-line summary # Shown in system prompt skill list
37
+ version: 1 # Optional
38
+ ---
39
+
40
+ Instructions body...
41
+ ```
42
+
43
+ ## Writing Good Skill Content
44
+
45
+ 1. **Start with context** — When would someone use this skill? What problem does it solve?
46
+ 2. **Use numbered steps** — Clear progression, one concept per step
47
+ 3. **Reference tools by name** — "Use `ReadModel.new.call(name: "User")`" not "check the model"
48
+ 4. **Show examples** — Concrete tool invocations the agent can copy
49
+ 5. **Explain reasoning** — Not just what to do, but why this order matters
50
+ 6. **Handle failure** — "If this step fails, try..." for common pitfalls
51
+ 7. **Keep focused** — One skill, one domain procedure. If it's too long, split into multiple skills.
52
+
53
+ ## Skill Naming Convention
54
+
55
+ ```
56
+ <domain>.<name>
57
+ ```
58
+
59
+ Examples:
60
+ - `rails.db_debug` — Rails-specific database debugging
61
+ - `github.pr_review` — GitHub PR review workflow
62
+ - `shell.patterns` — Shell tool composition patterns
63
+
64
+ ## Testing Your Skill
65
+
66
+ After creating a skill file, verify:
67
+ 1. Frontmatter has both `name` and `description`
68
+ 2. Name is lowercase with only letters, numbers, dots, hyphens
69
+ 3. Description is a complete sentence
70
+ 4. Instructions have numbered steps
71
+ 5. Tools are referenced by their full Ruby class names
72
+ 6. The skill directory contains `SKILL.md` (not `skill.md` or `README.md`)
@@ -0,0 +1,13 @@
1
+ module Ask
2
+ module Skills
3
+ Skill = Data.define(:name, :description, :instructions, :source) do
4
+ def to_s
5
+ "#{name}: #{description}"
6
+ end
7
+
8
+ def to_prompt_entry
9
+ "- **#{name}**: #{description}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ module Ask
2
+ module Skills
3
+ module Source
4
+ class Base
5
+ def load
6
+ raise NotImplementedError
7
+ end
8
+
9
+ def name
10
+ raise NotImplementedError
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,67 @@
1
+ module Ask
2
+ module Skills
3
+ module Source
4
+ class Filesystem < Base
5
+ def initialize(dir: nil, project_dir: nil, user_dir: nil)
6
+ @path = dir || project_dir || (user_dir ? File.expand_path(user_dir) : nil)
7
+ end
8
+
9
+ def load
10
+ return [] unless @path && Dir.exist?(@path)
11
+ skills = []
12
+ Dir.entries(@path).each do |entry|
13
+ next if entry.start_with?(".")
14
+ skill_dir = File.join(@path, entry)
15
+ next unless File.directory?(skill_dir)
16
+ skill_file = File.join(skill_dir, "SKILL.md")
17
+ next unless File.exist?(skill_file)
18
+ next if File.directory?(skill_file)
19
+ if (skill = parse_skill(skill_file))
20
+ skills << skill
21
+ end
22
+ end
23
+ skills
24
+ end
25
+
26
+ private
27
+
28
+ def parse_skill(path)
29
+ content = File.read(path)
30
+ frontmatter = parse_frontmatter(content)
31
+ body = extract_body(content)
32
+
33
+ name = frontmatter["name"] || File.basename(File.dirname(path))
34
+ description = frontmatter["description"] || ""
35
+
36
+ return nil if description.empty?
37
+
38
+ Skill.new(name: name, description: description, instructions: body, source: path)
39
+ end
40
+
41
+ def parse_frontmatter(content)
42
+ return {} unless content.start_with?("---\n")
43
+ end_idx = content.index("\n---\n", 4)
44
+ return {} unless end_idx
45
+ yaml_str = content[4...end_idx]
46
+ yaml = {}
47
+ yaml_str.split("\n").each do |line|
48
+ if (m = line.match(/\A(\w+):\s*(.+)\z/))
49
+ value = m[2].strip
50
+ value = value.gsub(/\A"|"\z/, "").gsub(/\A'|'\z/, "")
51
+ yaml[m[1]] = value
52
+ end
53
+ end
54
+ yaml
55
+ end
56
+
57
+ def extract_body(content)
58
+ return content unless content.start_with?("---\n")
59
+ end_idx = content.index("\n---\n", 4)
60
+ return content unless end_idx
61
+ body = content[(end_idx + 5)..] || ""
62
+ body.sub(/\A\n/, "").strip
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,62 @@
1
+ module Ask
2
+ module Skills
3
+ module Source
4
+ class Gems < Base
5
+ GLOB = "ask/skills/*/SKILL.md"
6
+
7
+ def name
8
+ "Gems"
9
+ end
10
+
11
+ def load
12
+ skills = []
13
+ Gem.find_files(GLOB).each do |path|
14
+ if (skill = parse_skill(path))
15
+ skills << skill
16
+ end
17
+ end
18
+ skills
19
+ end
20
+
21
+ private
22
+
23
+ def parse_skill(path)
24
+ content = File.read(path)
25
+ frontmatter = parse_frontmatter(content)
26
+ body = extract_body(content)
27
+
28
+ name = frontmatter["name"] || File.basename(File.dirname(path))
29
+ description = frontmatter["description"] || ""
30
+
31
+ return nil if description.empty?
32
+
33
+ Skill.new(name: name, description: description, instructions: body, source: path)
34
+ end
35
+
36
+ def parse_frontmatter(content)
37
+ return {} unless content.start_with?("---\n")
38
+ end_idx = content.index("\n---\n", 4)
39
+ return {} unless end_idx
40
+ yaml_str = content[4...end_idx]
41
+ yaml = {}
42
+ yaml_str.split("\n").each do |line|
43
+ if (m = line.match(/\A(\w+):\s*(.+)\z/))
44
+ value = m[2].strip
45
+ value = value.gsub(/\A"|"\z/, "").gsub(/\A'|'\z/, "")
46
+ yaml[m[1]] = value
47
+ end
48
+ end
49
+ yaml
50
+ end
51
+
52
+ def extract_body(content)
53
+ return content unless content.start_with?("---\n")
54
+ end_idx = content.index("\n---\n", 4)
55
+ return content unless end_idx
56
+ body = content[(end_idx + 5)..] || ""
57
+ body.sub(/\A\n/, "")
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,37 @@
1
+ module Ask
2
+ module Skills
3
+ class Validator
4
+ ValidationError = Data.define(:skill_name, :message)
5
+
6
+ NAME_PATTERN = /\A[a-z0-9_.-]+\z/
7
+
8
+ def initialize(skills)
9
+ @skills = skills
10
+ end
11
+
12
+ def validate_all
13
+ errors = []
14
+ names = {}
15
+ @skills.each do |skill|
16
+ errors.concat(validate(skill))
17
+ if names[skill.name]
18
+ errors << ValidationError.new(skill.name, "Duplicate skill name from #{names[skill.name]} and #{skill.source}")
19
+ end
20
+ names[skill.name] = skill.source
21
+ end
22
+ errors
23
+ end
24
+
25
+ def validate(skill)
26
+ errors = []
27
+ errors << ValidationError.new(skill.name, "Name is empty") if skill.name.empty?
28
+ errors << ValidationError.new(skill.name, "Description is empty") if skill.description.empty?
29
+ errors << ValidationError.new(skill.name, "Instructions are empty") if skill.instructions.strip.empty?
30
+ if !skill.name.empty? && skill.name !~ NAME_PATTERN
31
+ errors << ValidationError.new(skill.name, "Name must be lowercase, with only letters, numbers, dots, hyphens, underscores")
32
+ end
33
+ errors
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ module Ask
2
+ module Skills
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
data/lib/ask/skills.rb ADDED
@@ -0,0 +1,41 @@
1
+ require_relative "skills/version"
2
+
3
+ module Ask
4
+ module Skills
5
+ class Error < StandardError; end
6
+
7
+ autoload :Skill, "ask/skills/skill"
8
+ autoload :Registry, "ask/skills/registry"
9
+ autoload :Formatter, "ask/skills/formatter"
10
+ autoload :Validator, "ask/skills/validator"
11
+
12
+ module Source
13
+ autoload :Base, "ask/skills/sources/base"
14
+ autoload :Filesystem, "ask/skills/sources/filesystem"
15
+ autoload :Gems, "ask/skills/sources/gems"
16
+ end
17
+
18
+ class << self
19
+ def discover(sources: nil)
20
+ Registry.new(sources || default_sources)
21
+ end
22
+
23
+ def default_sources
24
+ [
25
+ # Highest priority first — first source wins in Registry
26
+ Source::Filesystem.new(project_dir: ".agents/skills"),
27
+ Source::Filesystem.new(user_dir: "~/.config/ask/skills"),
28
+ Source::Gems.new,
29
+ Source::Filesystem.new(dir: builtin_skills_dir),
30
+ ]
31
+ end
32
+
33
+ def builtin_skills_dir
34
+ # Skills live in lib/ask/skills/<skill_name>/SKILL.md
35
+ # __dir__ in this file (lib/ask/skills.rb) is lib/ask/
36
+ # The skill directories are in lib/ask/skills/
37
+ File.join(__dir__, "skills")
38
+ end
39
+ end
40
+ end
41
+ end
data/lib/ask-skills.rb ADDED
@@ -0,0 +1 @@
1
+ require_relative "ask/skills"
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ask-skills
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kaka Ruto
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.25'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.25'
26
+ - !ruby/object:Gem::Dependency
27
+ name: mocha
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.1'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ description: Discovers, validates, and formats agent skills from project directories,
55
+ user config, and installed gems. Ships built-in skills (codebase exploration, debugging
56
+ methodology). Each skill is a markdown file with step-by-step instructions that
57
+ the agent loads on demand.
58
+ email:
59
+ - kaka@myrrlabs.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - CHANGELOG.md
65
+ - LICENSE
66
+ - README.md
67
+ - lib/ask-skills.rb
68
+ - lib/ask/skills.rb
69
+ - lib/ask/skills/formatter.rb
70
+ - lib/ask/skills/registry.rb
71
+ - lib/ask/skills/skill.compose/SKILL.md
72
+ - lib/ask/skills/skill.design/SKILL.md
73
+ - lib/ask/skills/skill.rb
74
+ - lib/ask/skills/sources/base.rb
75
+ - lib/ask/skills/sources/filesystem.rb
76
+ - lib/ask/skills/sources/gems.rb
77
+ - lib/ask/skills/validator.rb
78
+ - lib/ask/skills/version.rb
79
+ homepage: https://github.com/ask-rb/ask-skills
80
+ licenses:
81
+ - MIT
82
+ metadata:
83
+ homepage_uri: https://github.com/ask-rb/ask-skills
84
+ source_code_uri: https://github.com/ask-rb/ask-skills
85
+ changelog_uri: https://github.com/ask-rb/ask-skills/blob/master/CHANGELOG.md
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '3.2'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubygems_version: 4.0.3
101
+ specification_version: 4
102
+ summary: Skill discovery and management for the ask-rb ecosystem
103
+ test_files: []