aidp 0.15.1 → 0.16.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 +14 -15
- 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 +853 -6
- 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 +20 -0
- 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/interactive_repl.rb +102 -11
- data/lib/aidp/execute/repl_macros.rb +776 -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/enhanced_runner.rb +16 -2
- data/lib/aidp/harness/error_handler.rb +1 -1
- data/lib/aidp/harness/provider_info.rb +20 -16
- data/lib/aidp/harness/provider_manager.rb +56 -49
- 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/init/doc_generator.rb +75 -10
- data/lib/aidp/init/project_analyzer.rb +154 -26
- data/lib/aidp/init/runner.rb +263 -10
- data/lib/aidp/jobs/background_runner.rb +15 -5
- data/lib/aidp/logger.rb +11 -0
- 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 +220 -0
- data/lib/aidp/skills/skill.rb +174 -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
- metadata +17 -7
- data/lib/aidp/analyze/prioritizer.rb +0 -403
- data/lib/aidp/analyze/report_generator.rb +0 -582
- data/lib/aidp/cli/checkpoint_command.rb +0 -98
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "../errors"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Skills
|
|
8
|
+
# Loads skills from SKILL.md files with YAML frontmatter
|
|
9
|
+
#
|
|
10
|
+
# Parses skill files in the format:
|
|
11
|
+
# ---
|
|
12
|
+
# id: skill_id
|
|
13
|
+
# name: Skill Name
|
|
14
|
+
# ...
|
|
15
|
+
# ---
|
|
16
|
+
# # Skill content in markdown
|
|
17
|
+
#
|
|
18
|
+
# @example Loading a skill
|
|
19
|
+
# skill = Loader.load_from_file("/path/to/SKILL.md")
|
|
20
|
+
#
|
|
21
|
+
# @example Loading a skill with provider filtering
|
|
22
|
+
# skill = Loader.load_from_file("/path/to/SKILL.md", provider: "anthropic")
|
|
23
|
+
class Loader
|
|
24
|
+
# Load a skill from a file path
|
|
25
|
+
#
|
|
26
|
+
# @param file_path [String] Path to SKILL.md file
|
|
27
|
+
# @param provider [String, nil] Optional provider name for compatibility check
|
|
28
|
+
# @return [Skill, nil] Loaded skill or nil if incompatible with provider
|
|
29
|
+
# @raise [Aidp::Errors::ValidationError] if file format is invalid
|
|
30
|
+
def self.load_from_file(file_path, provider: nil)
|
|
31
|
+
Aidp.log_debug("skills", "Loading skill from file", file: file_path, provider: provider)
|
|
32
|
+
|
|
33
|
+
unless File.exist?(file_path)
|
|
34
|
+
raise Aidp::Errors::ValidationError, "Skill file not found: #{file_path}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
content = File.read(file_path)
|
|
38
|
+
load_from_string(content, source_path: file_path, provider: provider)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Load a skill from a string
|
|
42
|
+
#
|
|
43
|
+
# @param content [String] SKILL.md file content
|
|
44
|
+
# @param source_path [String] Source file path for reference
|
|
45
|
+
# @param provider [String, nil] Optional provider name for compatibility check
|
|
46
|
+
# @return [Skill, nil] Loaded skill or nil if incompatible with provider
|
|
47
|
+
# @raise [Aidp::Errors::ValidationError] if format is invalid
|
|
48
|
+
def self.load_from_string(content, source_path:, provider: nil)
|
|
49
|
+
metadata, markdown = parse_frontmatter(content, source_path: source_path)
|
|
50
|
+
|
|
51
|
+
skill = Skill.new(
|
|
52
|
+
id: metadata["id"],
|
|
53
|
+
name: metadata["name"],
|
|
54
|
+
description: metadata["description"],
|
|
55
|
+
version: metadata["version"],
|
|
56
|
+
expertise: metadata["expertise"] || [],
|
|
57
|
+
keywords: metadata["keywords"] || [],
|
|
58
|
+
when_to_use: metadata["when_to_use"] || [],
|
|
59
|
+
when_not_to_use: metadata["when_not_to_use"] || [],
|
|
60
|
+
compatible_providers: metadata["compatible_providers"] || [],
|
|
61
|
+
content: markdown,
|
|
62
|
+
source_path: source_path
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Filter by provider compatibility if specified
|
|
66
|
+
if provider && !skill.compatible_with?(provider)
|
|
67
|
+
Aidp.log_debug(
|
|
68
|
+
"skills",
|
|
69
|
+
"Skipping incompatible skill",
|
|
70
|
+
skill_id: skill.id,
|
|
71
|
+
provider: provider,
|
|
72
|
+
compatible: skill.compatible_providers
|
|
73
|
+
)
|
|
74
|
+
return nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
Aidp.log_debug(
|
|
78
|
+
"skills",
|
|
79
|
+
"Loaded skill",
|
|
80
|
+
skill_id: skill.id,
|
|
81
|
+
version: skill.version,
|
|
82
|
+
source: source_path
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
skill
|
|
86
|
+
rescue Aidp::Errors::ValidationError => e
|
|
87
|
+
Aidp.log_error("skills", "Skill validation failed", error: e.message, file: source_path)
|
|
88
|
+
raise
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Load all skills from a directory
|
|
92
|
+
#
|
|
93
|
+
# @param directory [String] Path to directory containing skill subdirectories
|
|
94
|
+
# @param provider [String, nil] Optional provider name for compatibility check
|
|
95
|
+
# @return [Array<Skill>] Array of loaded skills (excludes incompatible)
|
|
96
|
+
def self.load_from_directory(directory, provider: nil)
|
|
97
|
+
Aidp.log_debug("skills", "Loading skills from directory", directory: directory, provider: provider)
|
|
98
|
+
|
|
99
|
+
unless Dir.exist?(directory)
|
|
100
|
+
Aidp.log_warn("skills", "Skills directory not found", directory: directory)
|
|
101
|
+
return []
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
skills = []
|
|
105
|
+
skill_dirs = Dir.glob(File.join(directory, "*")).select { |path| File.directory?(path) }
|
|
106
|
+
|
|
107
|
+
skill_dirs.each do |skill_dir|
|
|
108
|
+
skill_file = File.join(skill_dir, "SKILL.md")
|
|
109
|
+
next unless File.exist?(skill_file)
|
|
110
|
+
|
|
111
|
+
begin
|
|
112
|
+
skill = load_from_file(skill_file, provider: provider)
|
|
113
|
+
skills << skill if skill # nil if incompatible with provider
|
|
114
|
+
rescue Aidp::Errors::ValidationError => e
|
|
115
|
+
Aidp.log_warn(
|
|
116
|
+
"skills",
|
|
117
|
+
"Failed to load skill",
|
|
118
|
+
file: skill_file,
|
|
119
|
+
error: e.message
|
|
120
|
+
)
|
|
121
|
+
# Continue loading other skills even if one fails
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
Aidp.log_info(
|
|
126
|
+
"skills",
|
|
127
|
+
"Loaded skills from directory",
|
|
128
|
+
directory: directory,
|
|
129
|
+
count: skills.size
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
skills
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Parse YAML frontmatter from content
|
|
136
|
+
#
|
|
137
|
+
# @param content [String] File content with frontmatter
|
|
138
|
+
# @param source_path [String] Source path for error messages
|
|
139
|
+
# @return [Array(Hash, String)] Tuple of [metadata, markdown_content]
|
|
140
|
+
# @raise [Aidp::Errors::ValidationError] if frontmatter is missing or invalid
|
|
141
|
+
def self.parse_frontmatter(content, source_path:)
|
|
142
|
+
lines = content.lines
|
|
143
|
+
|
|
144
|
+
unless lines.first&.strip == "---"
|
|
145
|
+
raise Aidp::Errors::ValidationError,
|
|
146
|
+
"Invalid SKILL.md format: missing YAML frontmatter in #{source_path}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
frontmatter_lines = []
|
|
150
|
+
body_start_index = nil
|
|
151
|
+
|
|
152
|
+
lines[1..].each_with_index do |line, index|
|
|
153
|
+
if line.strip == "---"
|
|
154
|
+
body_start_index = index + 2
|
|
155
|
+
break
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
frontmatter_lines << line
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
unless body_start_index
|
|
162
|
+
raise Aidp::Errors::ValidationError,
|
|
163
|
+
"Invalid SKILL.md format: missing closing frontmatter delimiter in #{source_path}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
markdown_content = lines[body_start_index..]&.join.to_s.strip
|
|
167
|
+
frontmatter_yaml = frontmatter_lines.join
|
|
168
|
+
|
|
169
|
+
begin
|
|
170
|
+
metadata = YAML.safe_load(frontmatter_yaml, permitted_classes: [Symbol])
|
|
171
|
+
rescue Psych::SyntaxError => e
|
|
172
|
+
raise Aidp::Errors::ValidationError,
|
|
173
|
+
"Invalid YAML frontmatter in #{source_path}: #{e.message}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
unless metadata.is_a?(Hash)
|
|
177
|
+
raise Aidp::Errors::ValidationError,
|
|
178
|
+
"YAML frontmatter must be a hash in #{source_path}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
validate_required_fields(metadata, source_path: source_path)
|
|
182
|
+
|
|
183
|
+
[metadata, markdown_content]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Validate required frontmatter fields
|
|
187
|
+
#
|
|
188
|
+
# @param metadata [Hash] Parsed YAML metadata
|
|
189
|
+
# @param source_path [String] Source path for error messages
|
|
190
|
+
# @raise [Aidp::Errors::ValidationError] if required fields are missing
|
|
191
|
+
def self.validate_required_fields(metadata, source_path:)
|
|
192
|
+
required_fields = %w[id name description version]
|
|
193
|
+
|
|
194
|
+
required_fields.each do |field|
|
|
195
|
+
next if metadata[field] && !metadata[field].to_s.strip.empty?
|
|
196
|
+
|
|
197
|
+
raise Aidp::Errors::ValidationError,
|
|
198
|
+
"Missing required field '#{field}' in #{source_path}"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private_class_method :parse_frontmatter, :validate_required_fields
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,220 @@
|
|
|
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. Built-in skills directory (project_root/skills/)
|
|
12
|
+
# 2. Custom skills directory (.aidp/skills/)
|
|
13
|
+
#
|
|
14
|
+
# Custom skills with matching IDs override built-in 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. Built-in skills (project_root/skills/)
|
|
43
|
+
# 2. Custom skills (.aidp/skills/) - override built-in
|
|
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 built-in skills first
|
|
52
|
+
builtin_skills = load_from_path(builtin_skills_path)
|
|
53
|
+
builtin_skills.each { |skill| register_skill(skill, source: :builtin) }
|
|
54
|
+
|
|
55
|
+
# Load custom skills (override built-in if IDs match)
|
|
56
|
+
custom_skills = load_from_path(custom_skills_path)
|
|
57
|
+
custom_skills.each { |skill| register_skill(skill, source: :custom) }
|
|
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 :builtin and :custom arrays
|
|
143
|
+
def by_source
|
|
144
|
+
load_skills unless loaded?
|
|
145
|
+
|
|
146
|
+
{
|
|
147
|
+
builtin: @skills.values.select { |s| builtin_skill?(s) }.map(&:id),
|
|
148
|
+
custom: @skills.values.select { |s| custom_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 built-in skills path
|
|
190
|
+
#
|
|
191
|
+
# @return [String] Path to built-in skills directory
|
|
192
|
+
def builtin_skills_path
|
|
193
|
+
File.join(project_dir, "skills")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Get custom skills path
|
|
197
|
+
#
|
|
198
|
+
# @return [String] Path to custom skills directory
|
|
199
|
+
def custom_skills_path
|
|
200
|
+
File.join(project_dir, ".aidp", "skills")
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Check if skill is from built-in directory
|
|
204
|
+
#
|
|
205
|
+
# @param skill [Skill] Skill to check
|
|
206
|
+
# @return [Boolean] True if built-in
|
|
207
|
+
def builtin_skill?(skill)
|
|
208
|
+
skill.source_path.start_with?(builtin_skills_path)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Check if skill is from custom directory
|
|
212
|
+
#
|
|
213
|
+
# @param skill [Skill] Skill to check
|
|
214
|
+
# @return [Boolean] True if custom
|
|
215
|
+
def custom_skill?(skill)
|
|
216
|
+
skill.source_path.start_with?(custom_skills_path)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
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
|
data/lib/aidp/skills.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "skills/skill"
|
|
4
|
+
require_relative "skills/loader"
|
|
5
|
+
require_relative "skills/registry"
|
|
6
|
+
require_relative "skills/composer"
|
|
7
|
+
|
|
8
|
+
module Aidp
|
|
9
|
+
# Skills subsystem for managing agent personas and capabilities
|
|
10
|
+
#
|
|
11
|
+
# Skills define WHO the agent is (persona) and WHAT capabilities they have.
|
|
12
|
+
# This is separate from templates/procedures which define WHEN and HOW
|
|
13
|
+
# to execute specific tasks.
|
|
14
|
+
#
|
|
15
|
+
# @example Loading and using skills
|
|
16
|
+
# registry = Aidp::Skills::Registry.new(project_dir: Dir.pwd)
|
|
17
|
+
# registry.load_skills
|
|
18
|
+
#
|
|
19
|
+
# skill = registry.find("repository_analyst")
|
|
20
|
+
# composer = Aidp::Skills::Composer.new
|
|
21
|
+
# prompt = composer.compose(skill: skill, template: "Analyze the repo...")
|
|
22
|
+
#
|
|
23
|
+
# @example Creating a custom skill
|
|
24
|
+
# # Create .aidp/skills/my_skill/SKILL.md with YAML frontmatter
|
|
25
|
+
# # It will automatically override built-in skills with matching ID
|
|
26
|
+
module Skills
|
|
27
|
+
# Error raised when a skill is not found
|
|
28
|
+
class SkillNotFoundError < StandardError; end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/aidp/version.rb
CHANGED