aidp 0.15.2 → 0.17.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 +4 -4
- data/README.md +47 -0
- data/lib/aidp/analyze/error_handler.rb +46 -28
- data/lib/aidp/analyze/progress.rb +1 -1
- data/lib/aidp/analyze/runner.rb +27 -5
- data/lib/aidp/analyze/steps.rb +4 -0
- data/lib/aidp/cli/jobs_command.rb +2 -1
- data/lib/aidp/cli.rb +1086 -4
- data/lib/aidp/concurrency/backoff.rb +148 -0
- data/lib/aidp/concurrency/exec.rb +192 -0
- data/lib/aidp/concurrency/wait.rb +148 -0
- data/lib/aidp/concurrency.rb +71 -0
- data/lib/aidp/config.rb +21 -1
- data/lib/aidp/daemon/runner.rb +9 -8
- data/lib/aidp/debug_mixin.rb +1 -0
- data/lib/aidp/errors.rb +12 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
- data/lib/aidp/execute/checkpoint.rb +1 -1
- data/lib/aidp/execute/future_work_backlog.rb +1 -1
- data/lib/aidp/execute/interactive_repl.rb +102 -11
- data/lib/aidp/execute/progress.rb +1 -1
- data/lib/aidp/execute/repl_macros.rb +845 -2
- data/lib/aidp/execute/runner.rb +27 -5
- data/lib/aidp/execute/steps.rb +2 -0
- data/lib/aidp/harness/config_loader.rb +24 -2
- data/lib/aidp/harness/config_validator.rb +1 -1
- data/lib/aidp/harness/enhanced_runner.rb +16 -2
- data/lib/aidp/harness/error_handler.rb +1 -1
- data/lib/aidp/harness/provider_info.rb +19 -15
- data/lib/aidp/harness/provider_manager.rb +47 -41
- data/lib/aidp/harness/runner.rb +3 -11
- data/lib/aidp/harness/state/persistence.rb +1 -6
- data/lib/aidp/harness/state_manager.rb +115 -7
- data/lib/aidp/harness/status_display.rb +11 -18
- data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +1 -1
- data/lib/aidp/harness/user_interface.rb +12 -15
- data/lib/aidp/jobs/background_runner.rb +16 -6
- data/lib/aidp/providers/codex.rb +0 -1
- data/lib/aidp/providers/cursor.rb +0 -1
- data/lib/aidp/providers/github_copilot.rb +0 -1
- data/lib/aidp/providers/opencode.rb +0 -1
- data/lib/aidp/skills/composer.rb +178 -0
- data/lib/aidp/skills/loader.rb +205 -0
- data/lib/aidp/skills/registry.rb +222 -0
- data/lib/aidp/skills/router.rb +178 -0
- data/lib/aidp/skills/skill.rb +174 -0
- data/lib/aidp/skills/wizard/builder.rb +141 -0
- data/lib/aidp/skills/wizard/controller.rb +145 -0
- data/lib/aidp/skills/wizard/differ.rb +232 -0
- data/lib/aidp/skills/wizard/prompter.rb +317 -0
- data/lib/aidp/skills/wizard/template_library.rb +164 -0
- data/lib/aidp/skills/wizard/writer.rb +105 -0
- data/lib/aidp/skills.rb +30 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +93 -28
- data/lib/aidp/watch/runner.rb +3 -2
- data/lib/aidp/workstream_executor.rb +244 -0
- data/lib/aidp/workstream_state.rb +212 -0
- data/lib/aidp/worktree.rb +208 -0
- data/lib/aidp.rb +6 -0
- data/templates/skills/README.md +334 -0
- data/templates/skills/architecture_analyst/SKILL.md +173 -0
- data/templates/skills/product_strategist/SKILL.md +141 -0
- data/templates/skills/repository_analyst/SKILL.md +117 -0
- data/templates/skills/test_analyzer/SKILL.md +213 -0
- metadata +29 -4
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module Skills
|
|
5
|
+
# Registry for managing available skills
|
|
6
|
+
#
|
|
7
|
+
# The Registry loads skills from multiple search paths and provides
|
|
8
|
+
# lookup, filtering, and management capabilities.
|
|
9
|
+
#
|
|
10
|
+
# Skills are loaded from:
|
|
11
|
+
# 1. Template skills directory (gem templates/skills/) - built-in templates
|
|
12
|
+
# 2. Project skills directory (.aidp/skills/) - project-specific skills
|
|
13
|
+
#
|
|
14
|
+
# Project skills with matching IDs override template skills.
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage
|
|
17
|
+
# registry = Registry.new(project_dir: "/path/to/project")
|
|
18
|
+
# registry.load_skills
|
|
19
|
+
# skill = registry.find("repository_analyst")
|
|
20
|
+
#
|
|
21
|
+
# @example With provider filtering
|
|
22
|
+
# registry = Registry.new(project_dir: "/path/to/project", provider: "anthropic")
|
|
23
|
+
# registry.load_skills
|
|
24
|
+
# skills = registry.all # Only anthropic-compatible skills
|
|
25
|
+
class Registry
|
|
26
|
+
attr_reader :project_dir, :provider
|
|
27
|
+
|
|
28
|
+
# Initialize a new skills registry
|
|
29
|
+
#
|
|
30
|
+
# @param project_dir [String] Root directory of the project
|
|
31
|
+
# @param provider [String, nil] Optional provider name for filtering
|
|
32
|
+
def initialize(project_dir:, provider: nil)
|
|
33
|
+
@project_dir = project_dir
|
|
34
|
+
@provider = provider
|
|
35
|
+
@skills = {}
|
|
36
|
+
@loaded = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Load skills from all search paths
|
|
40
|
+
#
|
|
41
|
+
# Skills are loaded in order:
|
|
42
|
+
# 1. Template skills (gem templates/skills/) - built-in templates
|
|
43
|
+
# 2. Project skills (.aidp/skills/) - override templates
|
|
44
|
+
#
|
|
45
|
+
# @return [Integer] Number of skills loaded
|
|
46
|
+
def load_skills
|
|
47
|
+
Aidp.log_debug("skills", "Loading skills", project_dir: project_dir, provider: provider)
|
|
48
|
+
|
|
49
|
+
@skills = {}
|
|
50
|
+
|
|
51
|
+
# Load template skills first
|
|
52
|
+
template_skills = load_from_path(template_skills_path)
|
|
53
|
+
template_skills.each { |skill| register_skill(skill, source: :template) }
|
|
54
|
+
|
|
55
|
+
# Load project skills (override templates if IDs match)
|
|
56
|
+
project_skills = load_from_path(project_skills_path)
|
|
57
|
+
project_skills.each { |skill| register_skill(skill, source: :project) }
|
|
58
|
+
|
|
59
|
+
@loaded = true
|
|
60
|
+
|
|
61
|
+
Aidp.log_info("skills", "Loaded skills", count: @skills.size, provider: provider)
|
|
62
|
+
@skills.size
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Find a skill by ID
|
|
66
|
+
#
|
|
67
|
+
# @param skill_id [String] Skill identifier
|
|
68
|
+
# @return [Skill, nil] Skill if found, nil otherwise
|
|
69
|
+
def find(skill_id)
|
|
70
|
+
load_skills unless loaded?
|
|
71
|
+
@skills[skill_id.to_s]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get all registered skills
|
|
75
|
+
#
|
|
76
|
+
# @return [Array<Skill>] Array of all skills
|
|
77
|
+
def all
|
|
78
|
+
load_skills unless loaded?
|
|
79
|
+
@skills.values
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get skills matching a search query
|
|
83
|
+
#
|
|
84
|
+
# @param query [String] Search query (searches id, name, description, keywords, expertise)
|
|
85
|
+
# @return [Array<Skill>] Matching skills
|
|
86
|
+
def search(query)
|
|
87
|
+
load_skills unless loaded?
|
|
88
|
+
all.select { |skill| skill.matches?(query) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get skills by keyword
|
|
92
|
+
#
|
|
93
|
+
# @param keyword [String] Keyword to match
|
|
94
|
+
# @return [Array<Skill>] Skills with matching keyword
|
|
95
|
+
def by_keyword(keyword)
|
|
96
|
+
load_skills unless loaded?
|
|
97
|
+
all.select { |skill| skill.keywords.include?(keyword) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get skills compatible with a specific provider
|
|
101
|
+
#
|
|
102
|
+
# @param provider_name [String] Provider name
|
|
103
|
+
# @return [Array<Skill>] Compatible skills
|
|
104
|
+
def compatible_with(provider_name)
|
|
105
|
+
load_skills unless loaded?
|
|
106
|
+
all.select { |skill| skill.compatible_with?(provider_name) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Check if a skill exists
|
|
110
|
+
#
|
|
111
|
+
# @param skill_id [String] Skill identifier
|
|
112
|
+
# @return [Boolean] True if skill exists
|
|
113
|
+
def exists?(skill_id)
|
|
114
|
+
find(skill_id) != nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Check if skills have been loaded
|
|
118
|
+
#
|
|
119
|
+
# @return [Boolean] True if loaded
|
|
120
|
+
def loaded?
|
|
121
|
+
@loaded
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Get count of registered skills
|
|
125
|
+
#
|
|
126
|
+
# @return [Integer] Number of skills
|
|
127
|
+
def count
|
|
128
|
+
load_skills unless loaded?
|
|
129
|
+
@skills.size
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Reload skills from disk
|
|
133
|
+
#
|
|
134
|
+
# @return [Integer] Number of skills loaded
|
|
135
|
+
def reload
|
|
136
|
+
@loaded = false
|
|
137
|
+
load_skills
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Get skill IDs grouped by source
|
|
141
|
+
#
|
|
142
|
+
# @return [Hash] Hash with :template and :project arrays
|
|
143
|
+
def by_source
|
|
144
|
+
load_skills unless loaded?
|
|
145
|
+
|
|
146
|
+
{
|
|
147
|
+
template: @skills.values.select { |s| template_skill?(s) }.map(&:id),
|
|
148
|
+
project: @skills.values.select { |s| project_skill?(s) }.map(&:id)
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
# Register a skill in the registry
|
|
155
|
+
#
|
|
156
|
+
# @param skill [Skill] Skill to register
|
|
157
|
+
# @param source [Symbol] Source type (:builtin or :custom)
|
|
158
|
+
def register_skill(skill, source:)
|
|
159
|
+
if @skills.key?(skill.id)
|
|
160
|
+
Aidp.log_debug(
|
|
161
|
+
"skills",
|
|
162
|
+
"Overriding skill",
|
|
163
|
+
skill_id: skill.id,
|
|
164
|
+
old_source: @skills[skill.id].source_path,
|
|
165
|
+
new_source: skill.source_path
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
@skills[skill.id] = skill
|
|
170
|
+
|
|
171
|
+
Aidp.log_debug(
|
|
172
|
+
"skills",
|
|
173
|
+
"Registered skill",
|
|
174
|
+
skill_id: skill.id,
|
|
175
|
+
source: source,
|
|
176
|
+
version: skill.version
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Load skills from a directory path
|
|
181
|
+
#
|
|
182
|
+
# @param path [String] Directory path
|
|
183
|
+
# @return [Array<Skill>] Loaded skills
|
|
184
|
+
def load_from_path(path)
|
|
185
|
+
return [] unless Dir.exist?(path)
|
|
186
|
+
Loader.load_from_directory(path, provider: provider)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Get template skills path (from gem)
|
|
190
|
+
#
|
|
191
|
+
# @return [String] Path to template skills directory
|
|
192
|
+
def template_skills_path
|
|
193
|
+
# Get the gem root directory (go up from lib/aidp/skills/registry.rb)
|
|
194
|
+
gem_root = File.expand_path("../../../..", __dir__)
|
|
195
|
+
File.join(gem_root, "templates", "skills")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Get project skills path
|
|
199
|
+
#
|
|
200
|
+
# @return [String] Path to project-specific skills directory
|
|
201
|
+
def project_skills_path
|
|
202
|
+
File.join(project_dir, ".aidp", "skills")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Check if skill is from template directory
|
|
206
|
+
#
|
|
207
|
+
# @param skill [Skill] Skill to check
|
|
208
|
+
# @return [Boolean] True if template
|
|
209
|
+
def template_skill?(skill)
|
|
210
|
+
skill.source_path.start_with?(template_skills_path)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Check if skill is from project directory
|
|
214
|
+
#
|
|
215
|
+
# @param skill [Skill] Skill to check
|
|
216
|
+
# @return [Boolean] True if project
|
|
217
|
+
def project_skill?(skill)
|
|
218
|
+
skill.source_path.start_with?(project_skills_path)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Skills
|
|
7
|
+
# Routes file paths and tasks to appropriate skills based on routing rules
|
|
8
|
+
#
|
|
9
|
+
# The Router reads routing configuration from aidp.yml and matches:
|
|
10
|
+
# - File paths against glob patterns (path_rules)
|
|
11
|
+
# - Task descriptions against keywords (task_rules)
|
|
12
|
+
# - Combined path + task rules (combined_rules)
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# router = Router.new(project_dir: Dir.pwd)
|
|
16
|
+
# skill_id = router.route_by_path("app/controllers/users_controller.rb")
|
|
17
|
+
# # => "rails_expert"
|
|
18
|
+
#
|
|
19
|
+
# @example Task-based routing
|
|
20
|
+
# router = Router.new(project_dir: Dir.pwd)
|
|
21
|
+
# skill_id = router.route_by_task("Add API endpoint")
|
|
22
|
+
# # => "backend_developer"
|
|
23
|
+
#
|
|
24
|
+
# @example Combined routing
|
|
25
|
+
# router = Router.new(project_dir: Dir.pwd)
|
|
26
|
+
# skill_id = router.route(path: "lib/cli.rb", task: "Add command")
|
|
27
|
+
# # => "cli_expert"
|
|
28
|
+
class Router
|
|
29
|
+
attr_reader :config, :project_dir
|
|
30
|
+
|
|
31
|
+
# Initialize router with project directory
|
|
32
|
+
#
|
|
33
|
+
# @param project_dir [String] Path to project directory
|
|
34
|
+
def initialize(project_dir:)
|
|
35
|
+
@project_dir = project_dir
|
|
36
|
+
@config = load_config
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Route based on both path and task (highest priority)
|
|
40
|
+
#
|
|
41
|
+
# @param path [String, nil] File path to route
|
|
42
|
+
# @param task [String, nil] Task description to route
|
|
43
|
+
# @return [String, nil] Matched skill ID or nil
|
|
44
|
+
def route(path: nil, task: nil)
|
|
45
|
+
# Priority 1: Combined rules (path AND task)
|
|
46
|
+
if path && task
|
|
47
|
+
combined_match = match_combined_rules(path, task)
|
|
48
|
+
return combined_match if combined_match
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Priority 2: Path rules
|
|
52
|
+
path_match = route_by_path(path) if path
|
|
53
|
+
return path_match if path_match
|
|
54
|
+
|
|
55
|
+
# Priority 3: Task rules
|
|
56
|
+
task_match = route_by_task(task) if task
|
|
57
|
+
return task_match if task_match
|
|
58
|
+
|
|
59
|
+
# Priority 4: Default skill
|
|
60
|
+
config.dig("skills", "routing", "default")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Route based on file path
|
|
64
|
+
#
|
|
65
|
+
# @param path [String] File path to match against patterns
|
|
66
|
+
# @return [String, nil] Matched skill ID or nil
|
|
67
|
+
def route_by_path(path)
|
|
68
|
+
return nil unless path
|
|
69
|
+
|
|
70
|
+
path_rules = config.dig("skills", "routing", "path_rules") || {}
|
|
71
|
+
|
|
72
|
+
path_rules.each do |skill_id, patterns|
|
|
73
|
+
patterns = [patterns] unless patterns.is_a?(Array)
|
|
74
|
+
patterns.each do |pattern|
|
|
75
|
+
return skill_id if File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Route based on task description
|
|
83
|
+
#
|
|
84
|
+
# @param task [String] Task description to match against keywords
|
|
85
|
+
# @return [String, nil] Matched skill ID or nil
|
|
86
|
+
def route_by_task(task)
|
|
87
|
+
return nil unless task
|
|
88
|
+
|
|
89
|
+
task_rules = config.dig("skills", "routing", "task_rules") || {}
|
|
90
|
+
task_lower = task.downcase
|
|
91
|
+
|
|
92
|
+
task_rules.each do |skill_id, keywords|
|
|
93
|
+
keywords = [keywords] unless keywords.is_a?(Array)
|
|
94
|
+
keywords.each do |keyword|
|
|
95
|
+
return skill_id if task_lower.include?(keyword.downcase)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check if routing is enabled
|
|
103
|
+
#
|
|
104
|
+
# @return [Boolean] true if routing is configured
|
|
105
|
+
def routing_enabled?
|
|
106
|
+
config.dig("skills", "routing", "enabled") == true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Get default skill
|
|
110
|
+
#
|
|
111
|
+
# @return [String, nil] Default skill ID or nil
|
|
112
|
+
def default_skill
|
|
113
|
+
config.dig("skills", "routing", "default")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Get all routing rules
|
|
117
|
+
#
|
|
118
|
+
# @return [Hash] Hash containing path_rules, task_rules, and combined_rules
|
|
119
|
+
def rules
|
|
120
|
+
{
|
|
121
|
+
path_rules: config.dig("skills", "routing", "path_rules") || {},
|
|
122
|
+
task_rules: config.dig("skills", "routing", "task_rules") || {},
|
|
123
|
+
combined_rules: config.dig("skills", "routing", "combined_rules") || {}
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
# Match combined rules (path AND task must both match)
|
|
130
|
+
#
|
|
131
|
+
# @param path [String] File path
|
|
132
|
+
# @param task [String] Task description
|
|
133
|
+
# @return [String, nil] Matched skill ID or nil
|
|
134
|
+
def match_combined_rules(path, task)
|
|
135
|
+
combined_rules = config.dig("skills", "routing", "combined_rules") || {}
|
|
136
|
+
task_lower = task.downcase
|
|
137
|
+
|
|
138
|
+
combined_rules.each do |skill_id, rule|
|
|
139
|
+
path_patterns = rule["paths"] || []
|
|
140
|
+
task_keywords = rule["tasks"] || []
|
|
141
|
+
|
|
142
|
+
path_patterns = [path_patterns] unless path_patterns.is_a?(Array)
|
|
143
|
+
task_keywords = [task_keywords] unless task_keywords.is_a?(Array)
|
|
144
|
+
|
|
145
|
+
# Check if path matches any pattern
|
|
146
|
+
path_match = path_patterns.any? do |pattern|
|
|
147
|
+
File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Check if task matches any keyword
|
|
151
|
+
task_match = task_keywords.any? do |keyword|
|
|
152
|
+
task_lower.include?(keyword.downcase)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
return skill_id if path_match && task_match
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Load routing configuration from aidp.yml
|
|
162
|
+
#
|
|
163
|
+
# @return [Hash] Configuration hash
|
|
164
|
+
def load_config
|
|
165
|
+
config_path = File.join(project_dir, ".aidp", "aidp.yml")
|
|
166
|
+
|
|
167
|
+
if File.exist?(config_path)
|
|
168
|
+
YAML.safe_load_file(config_path, permitted_classes: [Date, Time, Symbol], aliases: true) || {}
|
|
169
|
+
else
|
|
170
|
+
{}
|
|
171
|
+
end
|
|
172
|
+
rescue => e
|
|
173
|
+
Aidp.log_error("skills", "Failed to load routing config", error: e.message)
|
|
174
|
+
{}
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Skills
|
|
7
|
+
# Represents a skill/persona with metadata and content
|
|
8
|
+
#
|
|
9
|
+
# A Skill encapsulates an agent's persona, expertise, and capabilities.
|
|
10
|
+
# Skills are loaded from SKILL.md files with YAML frontmatter.
|
|
11
|
+
#
|
|
12
|
+
# @example Creating a skill
|
|
13
|
+
# skill = Skill.new(
|
|
14
|
+
# id: "repository_analyst",
|
|
15
|
+
# name: "Repository Analyst",
|
|
16
|
+
# description: "Expert in version control analysis",
|
|
17
|
+
# version: "1.0.0",
|
|
18
|
+
# expertise: ["git analysis", "code metrics"],
|
|
19
|
+
# keywords: ["git", "metrics"],
|
|
20
|
+
# when_to_use: ["Analyzing repository history"],
|
|
21
|
+
# when_not_to_use: ["Writing new code"],
|
|
22
|
+
# compatible_providers: ["anthropic", "openai"],
|
|
23
|
+
# content: "You are a Repository Analyst...",
|
|
24
|
+
# source_path: "/path/to/SKILL.md"
|
|
25
|
+
# )
|
|
26
|
+
class Skill
|
|
27
|
+
attr_reader :id, :name, :description, :version, :expertise, :keywords,
|
|
28
|
+
:when_to_use, :when_not_to_use, :compatible_providers,
|
|
29
|
+
:content, :source_path
|
|
30
|
+
|
|
31
|
+
# Initialize a new Skill
|
|
32
|
+
#
|
|
33
|
+
# @param id [String] Unique identifier for the skill
|
|
34
|
+
# @param name [String] Human-readable name
|
|
35
|
+
# @param description [String] Brief one-line description
|
|
36
|
+
# @param version [String] Semantic version (e.g., "1.0.0")
|
|
37
|
+
# @param expertise [Array<String>] List of expertise areas
|
|
38
|
+
# @param keywords [Array<String>] Search/filter keywords
|
|
39
|
+
# @param when_to_use [Array<String>] Guidance for when to use this skill
|
|
40
|
+
# @param when_not_to_use [Array<String>] Guidance for when NOT to use
|
|
41
|
+
# @param compatible_providers [Array<String>] Compatible provider names
|
|
42
|
+
# @param content [String] The skill content (markdown)
|
|
43
|
+
# @param source_path [String] Path to source SKILL.md file
|
|
44
|
+
def initialize(
|
|
45
|
+
id:,
|
|
46
|
+
name:,
|
|
47
|
+
description:,
|
|
48
|
+
version:,
|
|
49
|
+
content:, source_path:, expertise: [],
|
|
50
|
+
keywords: [],
|
|
51
|
+
when_to_use: [],
|
|
52
|
+
when_not_to_use: [],
|
|
53
|
+
compatible_providers: []
|
|
54
|
+
)
|
|
55
|
+
@id = id
|
|
56
|
+
@name = name
|
|
57
|
+
@description = description
|
|
58
|
+
@version = version
|
|
59
|
+
@expertise = Array(expertise)
|
|
60
|
+
@keywords = Array(keywords)
|
|
61
|
+
@when_to_use = Array(when_to_use)
|
|
62
|
+
@when_not_to_use = Array(when_not_to_use)
|
|
63
|
+
@compatible_providers = Array(compatible_providers)
|
|
64
|
+
@content = content
|
|
65
|
+
@source_path = source_path
|
|
66
|
+
|
|
67
|
+
validate!
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Check if this skill is compatible with a given provider
|
|
71
|
+
#
|
|
72
|
+
# @param provider_name [String] Provider name (e.g., "anthropic")
|
|
73
|
+
# @return [Boolean] True if compatible or no restrictions defined
|
|
74
|
+
def compatible_with?(provider_name)
|
|
75
|
+
return true if compatible_providers.empty?
|
|
76
|
+
compatible_providers.include?(provider_name.to_s.downcase)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if this skill matches a search query
|
|
80
|
+
#
|
|
81
|
+
# Searches across: id, name, description, expertise, keywords
|
|
82
|
+
#
|
|
83
|
+
# @param query [String] Search query (case-insensitive)
|
|
84
|
+
# @return [Boolean] True if skill matches the query
|
|
85
|
+
def matches?(query)
|
|
86
|
+
return true if query.nil? || query.strip.empty?
|
|
87
|
+
|
|
88
|
+
query_lower = query.downcase
|
|
89
|
+
searchable_text = [
|
|
90
|
+
id,
|
|
91
|
+
name,
|
|
92
|
+
description,
|
|
93
|
+
expertise,
|
|
94
|
+
keywords
|
|
95
|
+
].flatten.join(" ").downcase
|
|
96
|
+
|
|
97
|
+
searchable_text.include?(query_lower)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get a summary of this skill for display
|
|
101
|
+
#
|
|
102
|
+
# @return [Hash] Summary with key skill metadata
|
|
103
|
+
def summary
|
|
104
|
+
{
|
|
105
|
+
id: id,
|
|
106
|
+
name: name,
|
|
107
|
+
description: description,
|
|
108
|
+
version: version,
|
|
109
|
+
expertise_areas: expertise.size,
|
|
110
|
+
keywords: keywords,
|
|
111
|
+
providers: compatible_providers.empty? ? "all" : compatible_providers.join(", ")
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Get full details of this skill for display
|
|
116
|
+
#
|
|
117
|
+
# @return [Hash] Detailed skill information
|
|
118
|
+
def details
|
|
119
|
+
{
|
|
120
|
+
id: id,
|
|
121
|
+
name: name,
|
|
122
|
+
description: description,
|
|
123
|
+
version: version,
|
|
124
|
+
expertise: expertise,
|
|
125
|
+
keywords: keywords,
|
|
126
|
+
when_to_use: when_to_use,
|
|
127
|
+
when_not_to_use: when_not_to_use,
|
|
128
|
+
compatible_providers: compatible_providers,
|
|
129
|
+
source: source_path,
|
|
130
|
+
content_length: content.length
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Return string representation
|
|
135
|
+
#
|
|
136
|
+
# @return [String] Skill representation
|
|
137
|
+
def to_s
|
|
138
|
+
"Skill[#{id}](#{name} v#{version})"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Return inspection string
|
|
142
|
+
#
|
|
143
|
+
# @return [String] Detailed inspection
|
|
144
|
+
def inspect
|
|
145
|
+
"#<Aidp::Skills::Skill id=#{id} name=\"#{name}\" version=#{version} " \
|
|
146
|
+
"source=#{source_path}>"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
# Validate required fields
|
|
152
|
+
#
|
|
153
|
+
# @raise [Aidp::Errors::ValidationError] if validation fails
|
|
154
|
+
def validate!
|
|
155
|
+
raise Aidp::Errors::ValidationError, "Skill id is required" if id.nil? || id.strip.empty?
|
|
156
|
+
raise Aidp::Errors::ValidationError, "Skill name is required" if name.nil? || name.strip.empty?
|
|
157
|
+
raise Aidp::Errors::ValidationError, "Skill description is required" if description.nil? || description.strip.empty?
|
|
158
|
+
raise Aidp::Errors::ValidationError, "Skill version is required" if version.nil? || version.strip.empty?
|
|
159
|
+
raise Aidp::Errors::ValidationError, "Skill content is required" if content.nil? || content.strip.empty?
|
|
160
|
+
raise Aidp::Errors::ValidationError, "Skill source_path is required" if source_path.nil? || source_path.strip.empty?
|
|
161
|
+
|
|
162
|
+
# Validate version format (simple semantic version check)
|
|
163
|
+
unless version.match?(/^\d+\.\d+\.\d+/)
|
|
164
|
+
raise Aidp::Errors::ValidationError, "Skill version must be in format X.Y.Z (e.g., 1.0.0)"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Validate id format (lowercase, alphanumeric, underscores only)
|
|
168
|
+
unless id.match?(/^[a-z0-9_]+$/)
|
|
169
|
+
raise Aidp::Errors::ValidationError, "Skill id must be lowercase alphanumeric with underscores only"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|