aidp 0.28.0 → 0.29.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/lib/aidp/cli/models_command.rb +75 -117
- data/lib/aidp/cli/tools_command.rb +333 -0
- data/lib/aidp/cli.rb +8 -1
- data/lib/aidp/config.rb +9 -0
- data/lib/aidp/execute/work_loop_runner.rb +6 -3
- data/lib/aidp/harness/capability_registry.rb +4 -4
- data/lib/aidp/harness/provider_manager.rb +5 -3
- data/lib/aidp/harness/ruby_llm_registry.rb +239 -0
- data/lib/aidp/metadata/cache.rb +201 -0
- data/lib/aidp/metadata/compiler.rb +229 -0
- data/lib/aidp/metadata/parser.rb +204 -0
- data/lib/aidp/metadata/query.rb +237 -0
- data/lib/aidp/metadata/scanner.rb +191 -0
- data/lib/aidp/metadata/tool_metadata.rb +245 -0
- data/lib/aidp/metadata/validator.rb +187 -0
- data/lib/aidp/providers/aider.rb +1 -1
- data/lib/aidp/providers/anthropic.rb +6 -4
- data/lib/aidp/providers/base.rb +105 -35
- data/lib/aidp/providers/codex.rb +1 -1
- data/lib/aidp/providers/cursor.rb +1 -1
- data/lib/aidp/providers/gemini.rb +1 -1
- data/lib/aidp/providers/github_copilot.rb +1 -1
- data/lib/aidp/providers/kilocode.rb +1 -1
- data/lib/aidp/providers/opencode.rb +1 -1
- data/lib/aidp/setup/wizard.rb +35 -107
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +11 -0
- data/templates/implementation/implement_features.md +4 -1
- metadata +24 -2
- data/lib/aidp/harness/model_discovery_service.rb +0 -259
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "parser"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Metadata
|
|
8
|
+
# Scans directories for tool files and extracts metadata
|
|
9
|
+
#
|
|
10
|
+
# Recursively finds all .md files in configured directories,
|
|
11
|
+
# parses their metadata, and returns a collection of ToolMetadata objects.
|
|
12
|
+
#
|
|
13
|
+
# @example Scanning directories
|
|
14
|
+
# scanner = Scanner.new([".aidp/skills", ".aidp/templates"])
|
|
15
|
+
# tools = scanner.scan_all
|
|
16
|
+
class Scanner
|
|
17
|
+
# Initialize scanner with directory paths
|
|
18
|
+
#
|
|
19
|
+
# @param directories [Array<String>] Directories to scan
|
|
20
|
+
def initialize(directories = [])
|
|
21
|
+
@directories = Array(directories)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Scan all configured directories
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<ToolMetadata>] All discovered tool metadata
|
|
27
|
+
def scan_all
|
|
28
|
+
Aidp.log_debug("metadata", "Scanning directories", directories: @directories)
|
|
29
|
+
|
|
30
|
+
all_tools = []
|
|
31
|
+
@directories.each do |dir|
|
|
32
|
+
tools = scan_directory(dir)
|
|
33
|
+
all_tools.concat(tools)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Aidp.log_info(
|
|
37
|
+
"metadata",
|
|
38
|
+
"Scan complete",
|
|
39
|
+
directories: @directories.size,
|
|
40
|
+
tools_found: all_tools.size
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
all_tools
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Scan a single directory
|
|
47
|
+
#
|
|
48
|
+
# @param directory [String] Directory path to scan
|
|
49
|
+
# @param type [String, nil] Tool type filter or nil for all
|
|
50
|
+
# @return [Array<ToolMetadata>] Discovered tool metadata
|
|
51
|
+
def scan_directory(directory, type: nil)
|
|
52
|
+
unless Dir.exist?(directory)
|
|
53
|
+
Aidp.log_warn("metadata", "Directory not found", directory: directory)
|
|
54
|
+
return []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Aidp.log_debug("metadata", "Scanning directory", directory: directory, type: type)
|
|
58
|
+
|
|
59
|
+
tools = []
|
|
60
|
+
md_files = find_markdown_files(directory)
|
|
61
|
+
|
|
62
|
+
md_files.each do |file_path|
|
|
63
|
+
tool = Parser.parse_file(file_path, type: type)
|
|
64
|
+
tools << tool if type.nil? || tool.type == type
|
|
65
|
+
rescue Aidp::Errors::ValidationError => e
|
|
66
|
+
Aidp.log_warn(
|
|
67
|
+
"metadata",
|
|
68
|
+
"Failed to parse file",
|
|
69
|
+
file: file_path,
|
|
70
|
+
error: e.message
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Aidp.log_debug(
|
|
75
|
+
"metadata",
|
|
76
|
+
"Directory scan complete",
|
|
77
|
+
directory: directory,
|
|
78
|
+
files_found: md_files.size,
|
|
79
|
+
tools_parsed: tools.size
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
tools
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Find all markdown files in directory recursively
|
|
86
|
+
#
|
|
87
|
+
# @param directory [String] Directory path
|
|
88
|
+
# @return [Array<String>] Paths to .md files
|
|
89
|
+
def find_markdown_files(directory)
|
|
90
|
+
pattern = File.join(directory, "**", "*.md")
|
|
91
|
+
files = Dir.glob(pattern)
|
|
92
|
+
|
|
93
|
+
Aidp.log_debug(
|
|
94
|
+
"metadata",
|
|
95
|
+
"Found markdown files",
|
|
96
|
+
directory: directory,
|
|
97
|
+
count: files.size
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
files
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Scan with file filtering
|
|
104
|
+
#
|
|
105
|
+
# @param directory [String] Directory path
|
|
106
|
+
# @param filter [Proc] Filter proc that receives file_path and returns boolean
|
|
107
|
+
# @return [Array<ToolMetadata>] Filtered tool metadata
|
|
108
|
+
def scan_with_filter(directory, &filter)
|
|
109
|
+
unless Dir.exist?(directory)
|
|
110
|
+
Aidp.log_warn("metadata", "Directory not found", directory: directory)
|
|
111
|
+
return []
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
tools = []
|
|
115
|
+
md_files = find_markdown_files(directory)
|
|
116
|
+
|
|
117
|
+
md_files.each do |file_path|
|
|
118
|
+
next unless filter.call(file_path)
|
|
119
|
+
|
|
120
|
+
begin
|
|
121
|
+
tool = Parser.parse_file(file_path)
|
|
122
|
+
tools << tool
|
|
123
|
+
rescue Aidp::Errors::ValidationError => e
|
|
124
|
+
Aidp.log_warn(
|
|
125
|
+
"metadata",
|
|
126
|
+
"Failed to parse file",
|
|
127
|
+
file: file_path,
|
|
128
|
+
error: e.message
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
tools
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Scan for changes since last scan
|
|
137
|
+
#
|
|
138
|
+
# Compares file hashes to detect changes
|
|
139
|
+
#
|
|
140
|
+
# @param directory [String] Directory path
|
|
141
|
+
# @param previous_hashes [Hash<String, String>] Map of file_path => file_hash
|
|
142
|
+
# @return [Hash] Hash with :added, :modified, :removed keys
|
|
143
|
+
def scan_changes(directory, previous_hashes = {})
|
|
144
|
+
Aidp.log_debug("metadata", "Scanning for changes", directory: directory)
|
|
145
|
+
|
|
146
|
+
current_files = find_markdown_files(directory)
|
|
147
|
+
current_hashes = {}
|
|
148
|
+
|
|
149
|
+
changes = {
|
|
150
|
+
added: [],
|
|
151
|
+
modified: [],
|
|
152
|
+
removed: [],
|
|
153
|
+
unchanged: []
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Check for added and modified files
|
|
157
|
+
current_files.each do |file_path|
|
|
158
|
+
content = File.read(file_path, encoding: "UTF-8")
|
|
159
|
+
file_hash = Parser.compute_file_hash(content)
|
|
160
|
+
current_hashes[file_path] = file_hash
|
|
161
|
+
|
|
162
|
+
if previous_hashes.key?(file_path)
|
|
163
|
+
if previous_hashes[file_path] != file_hash
|
|
164
|
+
changes[:modified] << file_path
|
|
165
|
+
else
|
|
166
|
+
changes[:unchanged] << file_path
|
|
167
|
+
end
|
|
168
|
+
else
|
|
169
|
+
changes[:added] << file_path
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Check for removed files
|
|
174
|
+
previous_hashes.keys.each do |file_path|
|
|
175
|
+
changes[:removed] << file_path unless current_hashes.key?(file_path)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
Aidp.log_info(
|
|
179
|
+
"metadata",
|
|
180
|
+
"Change detection complete",
|
|
181
|
+
added: changes[:added].size,
|
|
182
|
+
modified: changes[:modified].size,
|
|
183
|
+
removed: changes[:removed].size,
|
|
184
|
+
unchanged: changes[:unchanged].size
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
changes
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Metadata
|
|
7
|
+
# Base class for tool metadata (skills, personas, templates)
|
|
8
|
+
#
|
|
9
|
+
# Represents metadata extracted from YAML frontmatter in .md files.
|
|
10
|
+
# Provides validation and query capabilities for the tool directory.
|
|
11
|
+
#
|
|
12
|
+
# @example Creating tool metadata
|
|
13
|
+
# metadata = ToolMetadata.new(
|
|
14
|
+
# type: "skill",
|
|
15
|
+
# id: "ruby_rspec_tdd",
|
|
16
|
+
# title: "Ruby RSpec TDD Implementer",
|
|
17
|
+
# summary: "Expert in Test-Driven Development",
|
|
18
|
+
# version: "1.0.0",
|
|
19
|
+
# applies_to: ["ruby", "testing"],
|
|
20
|
+
# work_unit_types: ["implementation", "testing"],
|
|
21
|
+
# priority: 10,
|
|
22
|
+
# content: "You are a TDD expert...",
|
|
23
|
+
# source_path: "/path/to/SKILL.md"
|
|
24
|
+
# )
|
|
25
|
+
class ToolMetadata
|
|
26
|
+
attr_reader :type, :id, :title, :summary, :version,
|
|
27
|
+
:applies_to, :work_unit_types, :priority,
|
|
28
|
+
:capabilities, :dependencies, :experimental,
|
|
29
|
+
:content, :source_path, :file_hash
|
|
30
|
+
|
|
31
|
+
# Valid tool types
|
|
32
|
+
VALID_TYPES = %w[skill persona template].freeze
|
|
33
|
+
|
|
34
|
+
# Default priority (medium)
|
|
35
|
+
DEFAULT_PRIORITY = 5
|
|
36
|
+
|
|
37
|
+
# Initialize new tool metadata
|
|
38
|
+
#
|
|
39
|
+
# @param type [String] Tool type: "skill", "persona", or "template"
|
|
40
|
+
# @param id [String] Unique identifier (lowercase, alphanumeric, underscores)
|
|
41
|
+
# @param title [String] Human-readable title
|
|
42
|
+
# @param summary [String] Brief one-line summary
|
|
43
|
+
# @param version [String] Semantic version (e.g., "1.0.0")
|
|
44
|
+
# @param applies_to [Array<String>] Tags indicating applicability
|
|
45
|
+
# @param work_unit_types [Array<String>] Work unit types this tool supports
|
|
46
|
+
# @param priority [Integer] Priority for ranking (1-10, default 5)
|
|
47
|
+
# @param capabilities [Array<String>] Capabilities provided by this tool
|
|
48
|
+
# @param dependencies [Array<String>] IDs of required tools
|
|
49
|
+
# @param experimental [Boolean] Whether this is experimental
|
|
50
|
+
# @param content [String] The tool content (markdown)
|
|
51
|
+
# @param source_path [String] Path to source .md file
|
|
52
|
+
# @param file_hash [String] SHA256 hash of source file for cache invalidation
|
|
53
|
+
def initialize(
|
|
54
|
+
type:,
|
|
55
|
+
id:,
|
|
56
|
+
title:,
|
|
57
|
+
summary:,
|
|
58
|
+
version:,
|
|
59
|
+
content:,
|
|
60
|
+
source_path:,
|
|
61
|
+
file_hash:,
|
|
62
|
+
applies_to: [],
|
|
63
|
+
work_unit_types: [],
|
|
64
|
+
priority: DEFAULT_PRIORITY,
|
|
65
|
+
capabilities: [],
|
|
66
|
+
dependencies: [],
|
|
67
|
+
experimental: false
|
|
68
|
+
)
|
|
69
|
+
@type = type
|
|
70
|
+
@id = id
|
|
71
|
+
@title = title
|
|
72
|
+
@summary = summary
|
|
73
|
+
@version = version
|
|
74
|
+
@applies_to = Array(applies_to)
|
|
75
|
+
@work_unit_types = Array(work_unit_types)
|
|
76
|
+
@priority = priority
|
|
77
|
+
@capabilities = Array(capabilities)
|
|
78
|
+
@dependencies = Array(dependencies)
|
|
79
|
+
@experimental = experimental
|
|
80
|
+
@content = content
|
|
81
|
+
@source_path = source_path
|
|
82
|
+
@file_hash = file_hash
|
|
83
|
+
|
|
84
|
+
validate!
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check if this tool applies to given tags
|
|
88
|
+
#
|
|
89
|
+
# @param tags [Array<String>] Tags to check against
|
|
90
|
+
# @return [Boolean] True if any tag matches applies_to
|
|
91
|
+
def applies_to?(tags)
|
|
92
|
+
return true if applies_to.empty?
|
|
93
|
+
tags = Array(tags).map(&:downcase)
|
|
94
|
+
applies_to.map(&:downcase).any? { |tag| tags.include?(tag) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Check if this tool supports a given work unit type
|
|
98
|
+
#
|
|
99
|
+
# @param type [String] Work unit type (e.g., "implementation", "analysis")
|
|
100
|
+
# @return [Boolean] True if type is supported
|
|
101
|
+
def supports_work_unit?(type)
|
|
102
|
+
return true if work_unit_types.empty?
|
|
103
|
+
work_unit_types.map(&:downcase).include?(type.to_s.downcase)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get a summary of this tool for display
|
|
107
|
+
#
|
|
108
|
+
# @return [Hash] Summary with key metadata
|
|
109
|
+
def summary_hash
|
|
110
|
+
{
|
|
111
|
+
type: type,
|
|
112
|
+
id: id,
|
|
113
|
+
title: title,
|
|
114
|
+
summary: summary,
|
|
115
|
+
version: version,
|
|
116
|
+
priority: priority,
|
|
117
|
+
tags: applies_to,
|
|
118
|
+
experimental: experimental
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Get full details of this tool for display
|
|
123
|
+
#
|
|
124
|
+
# @return [Hash] Detailed tool information
|
|
125
|
+
def details
|
|
126
|
+
{
|
|
127
|
+
type: type,
|
|
128
|
+
id: id,
|
|
129
|
+
title: title,
|
|
130
|
+
summary: summary,
|
|
131
|
+
version: version,
|
|
132
|
+
applies_to: applies_to,
|
|
133
|
+
work_unit_types: work_unit_types,
|
|
134
|
+
priority: priority,
|
|
135
|
+
capabilities: capabilities,
|
|
136
|
+
dependencies: dependencies,
|
|
137
|
+
experimental: experimental,
|
|
138
|
+
source: source_path,
|
|
139
|
+
file_hash: file_hash,
|
|
140
|
+
content_length: content.length
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Convert to hash for serialization
|
|
145
|
+
#
|
|
146
|
+
# @return [Hash] Metadata as hash (for JSON)
|
|
147
|
+
def to_h
|
|
148
|
+
{
|
|
149
|
+
type: type,
|
|
150
|
+
id: id,
|
|
151
|
+
title: title,
|
|
152
|
+
summary: summary,
|
|
153
|
+
version: version,
|
|
154
|
+
applies_to: applies_to,
|
|
155
|
+
work_unit_types: work_unit_types,
|
|
156
|
+
priority: priority,
|
|
157
|
+
capabilities: capabilities,
|
|
158
|
+
dependencies: dependencies,
|
|
159
|
+
experimental: experimental,
|
|
160
|
+
source_path: source_path,
|
|
161
|
+
file_hash: file_hash
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Return string representation
|
|
166
|
+
#
|
|
167
|
+
# @return [String] Tool representation
|
|
168
|
+
def to_s
|
|
169
|
+
"#{type.capitalize}[#{id}](#{title} v#{version})"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Return inspection string
|
|
173
|
+
#
|
|
174
|
+
# @return [String] Detailed inspection
|
|
175
|
+
def inspect
|
|
176
|
+
"#<Aidp::Metadata::ToolMetadata type=#{type} id=#{id} title=\"#{title}\" " \
|
|
177
|
+
"version=#{version} source=#{source_path}>"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
# Validate required fields and types
|
|
183
|
+
#
|
|
184
|
+
# @raise [Aidp::Errors::ValidationError] if validation fails
|
|
185
|
+
def validate!
|
|
186
|
+
validate_required_fields!
|
|
187
|
+
validate_types!
|
|
188
|
+
validate_formats!
|
|
189
|
+
validate_ranges!
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Validate required fields are present
|
|
193
|
+
def validate_required_fields!
|
|
194
|
+
raise Aidp::Errors::ValidationError, "type is required" if type.nil? || type.strip.empty?
|
|
195
|
+
raise Aidp::Errors::ValidationError, "id is required" if id.nil? || id.strip.empty?
|
|
196
|
+
raise Aidp::Errors::ValidationError, "title is required" if title.nil? || title.strip.empty?
|
|
197
|
+
raise Aidp::Errors::ValidationError, "summary is required" if summary.nil? || summary.strip.empty?
|
|
198
|
+
raise Aidp::Errors::ValidationError, "version is required" if version.nil? || version.strip.empty?
|
|
199
|
+
raise Aidp::Errors::ValidationError, "content is required" if content.nil? || content.strip.empty?
|
|
200
|
+
raise Aidp::Errors::ValidationError, "source_path is required" if source_path.nil? || source_path.strip.empty?
|
|
201
|
+
raise Aidp::Errors::ValidationError, "file_hash is required" if file_hash.nil? || file_hash.strip.empty?
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Validate field types
|
|
205
|
+
def validate_types!
|
|
206
|
+
unless VALID_TYPES.include?(type)
|
|
207
|
+
raise Aidp::Errors::ValidationError, "type must be one of: #{VALID_TYPES.join(", ")}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
unless priority.is_a?(Integer)
|
|
211
|
+
raise Aidp::Errors::ValidationError, "priority must be an integer"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
unless [true, false].include?(experimental)
|
|
215
|
+
raise Aidp::Errors::ValidationError, "experimental must be boolean"
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Validate field formats
|
|
220
|
+
def validate_formats!
|
|
221
|
+
# Validate version format (simple semantic version check)
|
|
222
|
+
unless version.match?(/^\d+\.\d+\.\d+/)
|
|
223
|
+
raise Aidp::Errors::ValidationError, "version must be in format X.Y.Z (e.g., 1.0.0)"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Validate id format (lowercase, alphanumeric, underscores only)
|
|
227
|
+
unless id.match?(/^[a-z0-9_]+$/)
|
|
228
|
+
raise Aidp::Errors::ValidationError, "id must be lowercase alphanumeric with underscores only"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Validate file_hash format (SHA256 hex)
|
|
232
|
+
unless file_hash.match?(/^[a-f0-9]{64}$/)
|
|
233
|
+
raise Aidp::Errors::ValidationError, "file_hash must be a valid SHA256 hex string"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Validate value ranges
|
|
238
|
+
def validate_ranges!
|
|
239
|
+
unless priority.between?(1, 10)
|
|
240
|
+
raise Aidp::Errors::ValidationError, "priority must be between 1 and 10"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Metadata
|
|
7
|
+
# Validates tool metadata and detects issues
|
|
8
|
+
#
|
|
9
|
+
# Performs validation checks on tool metadata including:
|
|
10
|
+
# - Required field presence
|
|
11
|
+
# - Field type validation
|
|
12
|
+
# - Duplicate ID detection
|
|
13
|
+
# - Dependency resolution
|
|
14
|
+
# - Version format validation
|
|
15
|
+
#
|
|
16
|
+
# @example Validating a collection of tools
|
|
17
|
+
# validator = Validator.new(tools)
|
|
18
|
+
# results = validator.validate_all
|
|
19
|
+
# results.each { |r| puts "#{r[:file]}: #{r[:errors].join(", ")}" }
|
|
20
|
+
class Validator
|
|
21
|
+
# Validation result structure
|
|
22
|
+
ValidationResult = Struct.new(:tool_id, :file_path, :valid, :errors, :warnings, keyword_init: true)
|
|
23
|
+
|
|
24
|
+
# Initialize validator with tool metadata collection
|
|
25
|
+
#
|
|
26
|
+
# @param tools [Array<ToolMetadata>] Tools to validate
|
|
27
|
+
def initialize(tools = [])
|
|
28
|
+
@tools = tools
|
|
29
|
+
@errors_by_id = {}
|
|
30
|
+
@warnings_by_id = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Validate all tools
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<ValidationResult>] Validation results for each tool
|
|
36
|
+
def validate_all
|
|
37
|
+
Aidp.log_debug("metadata", "Validating all tools", count: @tools.size)
|
|
38
|
+
|
|
39
|
+
results = @tools.map do |tool|
|
|
40
|
+
validate_tool(tool)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Cross-tool validations
|
|
44
|
+
validate_duplicate_ids(results)
|
|
45
|
+
validate_dependencies(results)
|
|
46
|
+
|
|
47
|
+
Aidp.log_info(
|
|
48
|
+
"metadata",
|
|
49
|
+
"Validation complete",
|
|
50
|
+
total: results.size,
|
|
51
|
+
valid: results.count(&:valid),
|
|
52
|
+
invalid: results.count { |r| !r.valid }
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
results
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Validate a single tool
|
|
59
|
+
#
|
|
60
|
+
# @param tool [ToolMetadata] Tool to validate
|
|
61
|
+
# @return [ValidationResult] Validation result
|
|
62
|
+
def validate_tool(tool)
|
|
63
|
+
errors = []
|
|
64
|
+
warnings = []
|
|
65
|
+
|
|
66
|
+
# Tool metadata validates itself on initialization
|
|
67
|
+
# Here we add additional cross-cutting validations
|
|
68
|
+
|
|
69
|
+
# Check for empty arrays in key fields
|
|
70
|
+
if tool.applies_to.empty? && tool.work_unit_types.empty?
|
|
71
|
+
warnings << "No applies_to tags or work_unit_types specified (tool may not be discoverable)"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check for deprecated fields or patterns
|
|
75
|
+
validate_deprecated_patterns(tool, warnings)
|
|
76
|
+
|
|
77
|
+
# Check for experimental tools
|
|
78
|
+
if tool.experimental
|
|
79
|
+
warnings << "Tool is marked as experimental"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check content length
|
|
83
|
+
if tool.content.length < 100
|
|
84
|
+
warnings << "Tool content is very short (#{tool.content.length} characters)"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
ValidationResult.new(
|
|
88
|
+
tool_id: tool.id,
|
|
89
|
+
file_path: tool.source_path,
|
|
90
|
+
valid: errors.empty?,
|
|
91
|
+
errors: errors,
|
|
92
|
+
warnings: warnings
|
|
93
|
+
)
|
|
94
|
+
rescue Aidp::Errors::ValidationError => e
|
|
95
|
+
# Catch validation errors from tool initialization
|
|
96
|
+
ValidationResult.new(
|
|
97
|
+
tool_id: tool&.id || "unknown",
|
|
98
|
+
file_path: tool&.source_path || "unknown",
|
|
99
|
+
valid: false,
|
|
100
|
+
errors: [e.message],
|
|
101
|
+
warnings: []
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Validate for duplicate IDs across tools
|
|
106
|
+
#
|
|
107
|
+
# @param results [Array<ValidationResult>] Validation results
|
|
108
|
+
def validate_duplicate_ids(results)
|
|
109
|
+
ids = @tools.map(&:id)
|
|
110
|
+
duplicates = ids.tally.select { |_, count| count > 1 }.keys
|
|
111
|
+
|
|
112
|
+
return if duplicates.empty?
|
|
113
|
+
|
|
114
|
+
duplicates.each do |dup_id|
|
|
115
|
+
matching_tools = @tools.select { |t| t.id == dup_id }
|
|
116
|
+
matching_tools.each do |tool|
|
|
117
|
+
result = results.find { |r| r.tool_id == tool.id && r.file_path == tool.source_path }
|
|
118
|
+
next unless result
|
|
119
|
+
|
|
120
|
+
paths = matching_tools.map(&:source_path).join(", ")
|
|
121
|
+
result.errors << "Duplicate ID '#{dup_id}' found in: #{paths}"
|
|
122
|
+
result.valid = false
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
Aidp.log_warn("metadata", "Duplicate IDs detected", duplicates: duplicates)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Validate tool dependencies are satisfied
|
|
130
|
+
#
|
|
131
|
+
# @param results [Array<ValidationResult>] Validation results
|
|
132
|
+
def validate_dependencies(results)
|
|
133
|
+
available_ids = @tools.map(&:id).to_set
|
|
134
|
+
|
|
135
|
+
@tools.each do |tool|
|
|
136
|
+
next if tool.dependencies.empty?
|
|
137
|
+
|
|
138
|
+
tool.dependencies.each do |dep_id|
|
|
139
|
+
next if available_ids.include?(dep_id)
|
|
140
|
+
|
|
141
|
+
result = results.find { |r| r.tool_id == tool.id && r.file_path == tool.source_path }
|
|
142
|
+
next unless result
|
|
143
|
+
|
|
144
|
+
result.errors << "Missing dependency: '#{dep_id}'"
|
|
145
|
+
result.valid = false
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Check for deprecated patterns in tool metadata
|
|
151
|
+
#
|
|
152
|
+
# @param tool [ToolMetadata] Tool to check
|
|
153
|
+
# @param warnings [Array<String>] Warnings array to append to
|
|
154
|
+
def validate_deprecated_patterns(tool, warnings)
|
|
155
|
+
# Check for legacy field usage (this would be expanded based on actual deprecations)
|
|
156
|
+
# For now, this is a placeholder for future deprecation warnings
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Write validation errors to log file
|
|
160
|
+
#
|
|
161
|
+
# @param results [Array<ValidationResult>] Validation results
|
|
162
|
+
# @param log_path [String] Path to error log file
|
|
163
|
+
def write_error_log(results, log_path)
|
|
164
|
+
Aidp.log_debug("metadata", "Writing error log", path: log_path)
|
|
165
|
+
|
|
166
|
+
errors = results.reject(&:valid)
|
|
167
|
+
return if errors.empty?
|
|
168
|
+
|
|
169
|
+
File.open(log_path, "w") do |f|
|
|
170
|
+
f.puts "# Metadata Validation Errors"
|
|
171
|
+
f.puts "# Generated: #{Time.now.iso8601}"
|
|
172
|
+
f.puts
|
|
173
|
+
|
|
174
|
+
errors.each do |result|
|
|
175
|
+
f.puts "## #{result.tool_id} (#{result.file_path})"
|
|
176
|
+
f.puts
|
|
177
|
+
result.errors.each { |err| f.puts "- ERROR: #{err}" }
|
|
178
|
+
result.warnings.each { |warn| f.puts "- WARNING: #{warn}" }
|
|
179
|
+
f.puts
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
Aidp.log_info("metadata", "Wrote error log", path: log_path, error_count: errors.size)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
data/lib/aidp/providers/aider.rb
CHANGED
|
@@ -75,7 +75,7 @@ module Aidp
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
def send_message(prompt:, session: nil)
|
|
78
|
+
def send_message(prompt:, session: nil, options: {})
|
|
79
79
|
raise "aider CLI not available" unless self.class.available?
|
|
80
80
|
|
|
81
81
|
# Smart timeout calculation (store prompt length for adaptive logic)
|
|
@@ -9,6 +9,8 @@ module Aidp
|
|
|
9
9
|
class Anthropic < Base
|
|
10
10
|
include Aidp::DebugMixin
|
|
11
11
|
|
|
12
|
+
attr_reader :model
|
|
13
|
+
|
|
12
14
|
# Model name pattern for Anthropic Claude models
|
|
13
15
|
MODEL_PATTERN = /^claude-[\d.-]+-(?:opus|sonnet|haiku)(?:-\d{8})?$/i
|
|
14
16
|
|
|
@@ -250,13 +252,13 @@ module Aidp
|
|
|
250
252
|
}
|
|
251
253
|
end
|
|
252
254
|
|
|
253
|
-
def send_message(prompt:, session: nil)
|
|
255
|
+
def send_message(prompt:, session: nil, options: {})
|
|
254
256
|
raise "claude CLI not available" unless self.class.available?
|
|
255
257
|
|
|
256
|
-
# Smart timeout calculation
|
|
257
|
-
timeout_seconds = calculate_timeout
|
|
258
|
+
# Smart timeout calculation with tier awareness
|
|
259
|
+
timeout_seconds = calculate_timeout(options)
|
|
258
260
|
|
|
259
|
-
debug_provider("claude", "Starting execution", {timeout: timeout_seconds})
|
|
261
|
+
debug_provider("claude", "Starting execution", {timeout: timeout_seconds, tier: options[:tier]})
|
|
260
262
|
debug_log("📝 Sending prompt to claude...", level: :info)
|
|
261
263
|
|
|
262
264
|
# Build command arguments
|