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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +47 -0
  3. data/lib/aidp/analyze/error_handler.rb +14 -15
  4. data/lib/aidp/analyze/runner.rb +27 -5
  5. data/lib/aidp/analyze/steps.rb +4 -0
  6. data/lib/aidp/cli/jobs_command.rb +2 -1
  7. data/lib/aidp/cli.rb +853 -6
  8. data/lib/aidp/concurrency/backoff.rb +148 -0
  9. data/lib/aidp/concurrency/exec.rb +192 -0
  10. data/lib/aidp/concurrency/wait.rb +148 -0
  11. data/lib/aidp/concurrency.rb +71 -0
  12. data/lib/aidp/config.rb +20 -0
  13. data/lib/aidp/daemon/runner.rb +9 -8
  14. data/lib/aidp/debug_mixin.rb +1 -0
  15. data/lib/aidp/errors.rb +12 -0
  16. data/lib/aidp/execute/interactive_repl.rb +102 -11
  17. data/lib/aidp/execute/repl_macros.rb +776 -2
  18. data/lib/aidp/execute/runner.rb +27 -5
  19. data/lib/aidp/execute/steps.rb +2 -0
  20. data/lib/aidp/harness/config_loader.rb +24 -2
  21. data/lib/aidp/harness/enhanced_runner.rb +16 -2
  22. data/lib/aidp/harness/error_handler.rb +1 -1
  23. data/lib/aidp/harness/provider_info.rb +20 -16
  24. data/lib/aidp/harness/provider_manager.rb +56 -49
  25. data/lib/aidp/harness/runner.rb +3 -11
  26. data/lib/aidp/harness/state/persistence.rb +1 -6
  27. data/lib/aidp/harness/state_manager.rb +115 -7
  28. data/lib/aidp/harness/status_display.rb +11 -18
  29. data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
  30. data/lib/aidp/harness/ui/workflow_controller.rb +1 -1
  31. data/lib/aidp/harness/user_interface.rb +12 -15
  32. data/lib/aidp/init/doc_generator.rb +75 -10
  33. data/lib/aidp/init/project_analyzer.rb +154 -26
  34. data/lib/aidp/init/runner.rb +263 -10
  35. data/lib/aidp/jobs/background_runner.rb +15 -5
  36. data/lib/aidp/logger.rb +11 -0
  37. data/lib/aidp/providers/codex.rb +0 -1
  38. data/lib/aidp/providers/cursor.rb +0 -1
  39. data/lib/aidp/providers/github_copilot.rb +0 -1
  40. data/lib/aidp/providers/opencode.rb +0 -1
  41. data/lib/aidp/skills/composer.rb +178 -0
  42. data/lib/aidp/skills/loader.rb +205 -0
  43. data/lib/aidp/skills/registry.rb +220 -0
  44. data/lib/aidp/skills/skill.rb +174 -0
  45. data/lib/aidp/skills.rb +30 -0
  46. data/lib/aidp/version.rb +1 -1
  47. data/lib/aidp/watch/build_processor.rb +93 -28
  48. data/lib/aidp/watch/runner.rb +3 -2
  49. data/lib/aidp/workstream_executor.rb +244 -0
  50. data/lib/aidp/workstream_state.rb +212 -0
  51. data/lib/aidp/worktree.rb +208 -0
  52. data/lib/aidp.rb +6 -0
  53. metadata +17 -7
  54. data/lib/aidp/analyze/prioritizer.rb +0 -403
  55. data/lib/aidp/analyze/report_generator.rb +0 -582
  56. 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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.15.1"
4
+ VERSION = "0.16.0"
5
5
  end