aidp 0.41.0 → 0.43.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/providers_command.rb +16 -1
- data/lib/aidp/config_paths.rb +1 -0
- data/lib/aidp/execute/project_knowledge_manager.rb +300 -0
- data/lib/aidp/execute/work_loop_runner.rb +293 -15
- data/lib/aidp/prompt_optimization/context_composer.rb +5 -0
- data/lib/aidp/prompt_optimization/optimizer.rb +11 -0
- data/lib/aidp/prompt_optimization/project_knowledge_indexer.rb +98 -0
- data/lib/aidp/prompt_optimization/prompt_builder.rb +22 -0
- data/lib/aidp/prompt_optimization/relevance_scorer.rb +17 -0
- data/lib/aidp/security/mcp_risk_profile.rb +110 -0
- data/lib/aidp/security/mcp_tool_risk_classifier.rb +233 -0
- data/lib/aidp/security/work_loop_adapter.rb +93 -2
- data/lib/aidp/security.rb +2 -0
- data/lib/aidp/setup/wizard.rb +13 -0
- data/lib/aidp/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 013dc044889cc320404776d0691d2091f3cddda0d9ba854dc0afb92312396336
|
|
4
|
+
data.tar.gz: bb1c57bf8e7d87dfa7d7b169d69646ac76c0cb14fe2f72c70ef02b8543b43494
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c7ea760cd1d62424ada60f64acc356cdf4a82db6e2166c203d9d41c489fed936a704afaee17e9a20d588fbf0ead8ebd816a991a3fa01d87644939c872b104b12
|
|
7
|
+
data.tar.gz: 9c3b34d7474a2f9e899bf50d9b61d1edcb67a1b4a7b41d78abf810517db8cb2082fe889ec7b1fda5266276b6f1406e577644f2ddca16142bcf66e66e79b1e338
|
|
@@ -6,6 +6,7 @@ require "tty-spinner"
|
|
|
6
6
|
require_relative "../harness/provider_info"
|
|
7
7
|
require_relative "../harness/capability_registry"
|
|
8
8
|
require_relative "../harness/config_manager"
|
|
9
|
+
require_relative "../security"
|
|
9
10
|
|
|
10
11
|
module Aidp
|
|
11
12
|
class CLI
|
|
@@ -21,12 +22,13 @@ module Aidp
|
|
|
21
22
|
class ProvidersCommand
|
|
22
23
|
include Aidp::MessageDisplay
|
|
23
24
|
|
|
24
|
-
def initialize(prompt: TTY::Prompt.new, provider_info_class: nil, capability_registry_class: nil, config_manager_class: nil, project_dir: nil)
|
|
25
|
+
def initialize(prompt: TTY::Prompt.new, provider_info_class: nil, capability_registry_class: nil, config_manager_class: nil, project_dir: nil, mcp_tool_risk_classifier_class: nil)
|
|
25
26
|
@prompt = prompt
|
|
26
27
|
@provider_info_class = provider_info_class || Aidp::Harness::ProviderInfo
|
|
27
28
|
@capability_registry_class = capability_registry_class || Aidp::Harness::CapabilityRegistry
|
|
28
29
|
@config_manager_class = config_manager_class || Aidp::Harness::ConfigManager
|
|
29
30
|
@project_dir = project_dir || Dir.pwd
|
|
31
|
+
@mcp_tool_risk_classifier_class = mcp_tool_risk_classifier_class || Aidp::Security::McpToolRiskClassifier
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
# Main entry point for providers info/refresh subcommands
|
|
@@ -198,10 +200,23 @@ module Aidp
|
|
|
198
200
|
end
|
|
199
201
|
end
|
|
200
202
|
|
|
203
|
+
refresh_mcp_risk_profile(config_manager, providers_to_refresh)
|
|
204
|
+
|
|
201
205
|
display_message("\n" + "=" * 60, type: :muted)
|
|
202
206
|
display_message("Refresh complete", type: :highlight)
|
|
203
207
|
end
|
|
204
208
|
|
|
209
|
+
def refresh_mcp_risk_profile(config_manager, providers_to_refresh)
|
|
210
|
+
classifier = @mcp_tool_risk_classifier_class.new(config_manager, project_dir: @project_dir)
|
|
211
|
+
classifier.generate!(
|
|
212
|
+
providers: config_manager.provider_names,
|
|
213
|
+
force_refresh_providers: providers_to_refresh
|
|
214
|
+
)
|
|
215
|
+
display_message("✓ MCP risk profile refreshed", type: :success)
|
|
216
|
+
rescue => e
|
|
217
|
+
display_message("⚠ MCP risk profile refresh failed: #{e.message}", type: :warning)
|
|
218
|
+
end
|
|
219
|
+
|
|
205
220
|
def display_help
|
|
206
221
|
display_message("\nUsage: aidp providers <subcommand> [options]", type: :info)
|
|
207
222
|
display_message("\nSubcommands:", type: :info)
|
data/lib/aidp/config_paths.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Aidp
|
|
|
32
32
|
# Security module paths
|
|
33
33
|
def self.security_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "security")
|
|
34
34
|
def self.secrets_registry_file(project_dir = Dir.pwd) = File.join(security_dir(project_dir), "secrets_registry.json")
|
|
35
|
+
def self.mcp_risk_profile_file(project_dir = Dir.pwd) = File.join(security_dir(project_dir), "mcp_risk_profile.yml")
|
|
35
36
|
|
|
36
37
|
# Database path
|
|
37
38
|
def self.database_file(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "aidp.db")
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Execute
|
|
8
|
+
# Creates and refreshes concise feature/tool notes under docs/
|
|
9
|
+
class ProjectKnowledgeManager
|
|
10
|
+
MAX_TOOL_NOTES = 5
|
|
11
|
+
FEATURE_ROOT_SEGMENTS = %w[app client lib packages server src].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(project_dir)
|
|
14
|
+
@project_dir = project_dir
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def sync!(step_name:, task_description:, affected_files:, tool_commands:, feature_identifier: nil)
|
|
18
|
+
sync_feature_note(
|
|
19
|
+
feature_identifier: feature_identifier,
|
|
20
|
+
task_description: task_description,
|
|
21
|
+
affected_files: affected_files
|
|
22
|
+
)
|
|
23
|
+
sync_tool_notes(step_name: step_name, affected_files: affected_files, tool_commands: tool_commands)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def sync_feature_note(feature_identifier:, task_description:, affected_files:)
|
|
29
|
+
return if affected_files.empty?
|
|
30
|
+
|
|
31
|
+
feature_id = resolve_feature_id(
|
|
32
|
+
feature_identifier: feature_identifier,
|
|
33
|
+
affected_files: affected_files
|
|
34
|
+
)
|
|
35
|
+
return if feature_id.empty?
|
|
36
|
+
|
|
37
|
+
path = File.join(@project_dir, "docs", "features", "#{feature_id}.md")
|
|
38
|
+
markers = affected_files.map { |file| "<#{feature_id}:#{file}>" }
|
|
39
|
+
learning = bullet_line("#{Time.now.utc.iso8601}: #{learning_summary(task_description)}")
|
|
40
|
+
|
|
41
|
+
content = if File.exist?(path)
|
|
42
|
+
update_existing_note(File.read(path, encoding: "UTF-8"), markers: markers, learning: learning)
|
|
43
|
+
else
|
|
44
|
+
build_feature_note(feature_id: feature_id, markers: markers, learning: learning)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
write_note(path, content)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def sync_tool_notes(step_name:, affected_files:, tool_commands:)
|
|
51
|
+
tool_commands.first(MAX_TOOL_NOTES).each do |command|
|
|
52
|
+
tool_id = tool_identifier(command)
|
|
53
|
+
next if tool_id.empty?
|
|
54
|
+
|
|
55
|
+
path = File.join(@project_dir, "docs", "tools", "#{tool_id}.md")
|
|
56
|
+
commands = [bullet_line("`#{command}`")]
|
|
57
|
+
markers = affected_files.map { |file| "<#{tool_id}:#{file}>" }
|
|
58
|
+
learning = bullet_line("#{Time.now.utc.iso8601}: Used `#{command}` during `#{step_name}`.")
|
|
59
|
+
|
|
60
|
+
content = if File.exist?(path)
|
|
61
|
+
update_existing_note(File.read(path, encoding: "UTF-8"), markers: markers, learning: learning, commands: commands)
|
|
62
|
+
else
|
|
63
|
+
build_tool_note(commands: commands, tool_id: tool_id, markers: markers, learning: learning)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
write_note(path, content)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_feature_note(feature_id:, markers:, learning:)
|
|
71
|
+
<<~MARKDOWN
|
|
72
|
+
# #{humanize(feature_id)}
|
|
73
|
+
|
|
74
|
+
## Paths
|
|
75
|
+
#{markers.join("\n")}
|
|
76
|
+
|
|
77
|
+
## Purpose
|
|
78
|
+
- Concise feature context for work touching these paths.
|
|
79
|
+
|
|
80
|
+
## Users
|
|
81
|
+
- Add the end users or personas when they become clear.
|
|
82
|
+
|
|
83
|
+
## Known Gaps
|
|
84
|
+
- Add concrete gaps when discovered.
|
|
85
|
+
|
|
86
|
+
## Key Decisions
|
|
87
|
+
- Add behavior-affecting decisions when they become clear.
|
|
88
|
+
|
|
89
|
+
## Recent Learnings
|
|
90
|
+
#{learning}
|
|
91
|
+
MARKDOWN
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_tool_note(commands:, tool_id:, markers:, learning:)
|
|
95
|
+
paths_section = markers.empty? ? "- Add affected paths as they become known." : markers.join("\n")
|
|
96
|
+
|
|
97
|
+
<<~MARKDOWN
|
|
98
|
+
# #{humanize(tool_id)}
|
|
99
|
+
|
|
100
|
+
## Commands
|
|
101
|
+
#{commands.join("\n")}
|
|
102
|
+
|
|
103
|
+
## Paths
|
|
104
|
+
#{paths_section}
|
|
105
|
+
|
|
106
|
+
## Intended Use
|
|
107
|
+
- Run this tool while validating or updating related paths.
|
|
108
|
+
|
|
109
|
+
## Known Gaps
|
|
110
|
+
- Add environment quirks or failure modes when discovered.
|
|
111
|
+
|
|
112
|
+
## Recent Learnings
|
|
113
|
+
#{learning}
|
|
114
|
+
MARKDOWN
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def update_existing_note(content, markers:, learning:, commands: [])
|
|
118
|
+
updated = ensure_section_entries(content, "Commands", commands)
|
|
119
|
+
updated = ensure_section_entries(updated, "Paths", markers)
|
|
120
|
+
ensure_section_entries(updated, "Recent Learnings", [learning], normalizer: method(:normalize_learning_entry))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def ensure_section_entries(content, section_name, entries, normalizer: nil)
|
|
124
|
+
return content if entries.empty?
|
|
125
|
+
|
|
126
|
+
existing_section = content.match(/(## #{Regexp.escape(section_name)}\n)(.*?)(?=\n## |\z)/m)
|
|
127
|
+
return append_new_section(content, section_name, entries) unless existing_section
|
|
128
|
+
|
|
129
|
+
body = existing_section[2].to_s
|
|
130
|
+
existing_entries = body.lines.map(&:strip).reject(&:empty?)
|
|
131
|
+
normalized_existing = normalize_section_entries(existing_entries, normalizer)
|
|
132
|
+
missing = entries.reject do |entry|
|
|
133
|
+
normalized_existing.include?(normalize_section_entry(entry, normalizer))
|
|
134
|
+
end
|
|
135
|
+
return content if missing.empty?
|
|
136
|
+
|
|
137
|
+
replacement = "#{existing_section[1]}#{body.rstrip}\n#{missing.join("\n")}\n"
|
|
138
|
+
content.sub(existing_section[0], replacement)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def append_new_section(content, section_name, entries)
|
|
142
|
+
[content.rstrip, "", "## #{section_name}", entries.join("\n"), ""].join("\n")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def write_note(path, content)
|
|
146
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
147
|
+
File.write(path, content)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def resolve_feature_id(feature_identifier:, affected_files:)
|
|
151
|
+
explicit_id = normalize_id(feature_identifier)
|
|
152
|
+
return explicit_id unless explicit_id.empty?
|
|
153
|
+
|
|
154
|
+
derived_id = normalize_id(feature_path_identifier(affected_files))
|
|
155
|
+
return derived_id unless derived_id.empty?
|
|
156
|
+
|
|
157
|
+
""
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def feature_path_identifier(affected_files)
|
|
161
|
+
feature_files = affected_files.select { |file| feature_path?(file) }
|
|
162
|
+
segments = feature_files.map { |file| trimmed_feature_segments(file) }
|
|
163
|
+
return "" if segments.empty?
|
|
164
|
+
|
|
165
|
+
shared_segments = meaningful_segments(common_path_segments(segments))
|
|
166
|
+
return shared_segments.join("_") unless shared_segments.empty?
|
|
167
|
+
|
|
168
|
+
meaningful_segments(segments.first).join("_")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def feature_path?(file)
|
|
172
|
+
path_segments = file.to_s.split("/")
|
|
173
|
+
FEATURE_ROOT_SEGMENTS.include?(path_segments.first) && path_segments.length > 1
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def common_path_segments(all_segments)
|
|
177
|
+
shared = []
|
|
178
|
+
max_length = all_segments.map(&:length).min
|
|
179
|
+
|
|
180
|
+
max_length.times do |index|
|
|
181
|
+
segment = all_segments.first[index]
|
|
182
|
+
break unless all_segments.all? { |segments| segments[index] == segment }
|
|
183
|
+
|
|
184
|
+
shared << segment
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
shared
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def meaningful_segments(path_segments)
|
|
191
|
+
path_segments.map { |segment| segment.sub(/\.[^.]+\z/, "") }
|
|
192
|
+
.reject { |segment| generic_path_segment?(segment) }
|
|
193
|
+
.reject(&:empty?)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def trimmed_feature_segments(file)
|
|
197
|
+
segments = file.to_s.split("/")
|
|
198
|
+
return segments unless generic_path_segment?(segments.first)
|
|
199
|
+
|
|
200
|
+
segments.drop(1)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def generic_path_segment?(segment)
|
|
204
|
+
%w[app client docs lib packages server spec src test].include?(segment)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def normalize_id(value)
|
|
208
|
+
value.to_s.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/\A_|_\z/, "")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def learning_summary(task_description)
|
|
212
|
+
single_line = task_description.to_s.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
213
|
+
.lines
|
|
214
|
+
.map(&:strip)
|
|
215
|
+
.reject(&:empty?)
|
|
216
|
+
.join(" ")
|
|
217
|
+
.gsub(/\s+/, " ")
|
|
218
|
+
|
|
219
|
+
return "Updated feature context." if single_line.empty?
|
|
220
|
+
return single_line if single_line.length <= 200
|
|
221
|
+
|
|
222
|
+
"#{single_line[0, 197].rstrip}..."
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def normalize_section_entries(entries, normalizer)
|
|
226
|
+
entries.map { |entry| normalize_section_entry(entry, normalizer) }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def normalize_section_entry(entry, normalizer)
|
|
230
|
+
return entry unless normalizer
|
|
231
|
+
|
|
232
|
+
normalizer.call(entry)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def normalize_learning_entry(entry)
|
|
236
|
+
entry.to_s.sub(/\A-\s*/, "").sub(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z:\s*/, "")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def tool_identifier(command)
|
|
240
|
+
raw_command = command.to_s.strip
|
|
241
|
+
tokens = Shellwords.split(raw_command)
|
|
242
|
+
executable = unwrap_tool_command(strip_env_assignments(tokens))
|
|
243
|
+
normalize_id(executable)
|
|
244
|
+
rescue ArgumentError
|
|
245
|
+
normalize_id(raw_command.split(/\s+/).first)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def unwrap_tool_command(tokens)
|
|
249
|
+
command_tokens = tokens.dup
|
|
250
|
+
|
|
251
|
+
loop do
|
|
252
|
+
return "" if command_tokens.empty?
|
|
253
|
+
|
|
254
|
+
case command_tokens.first
|
|
255
|
+
when "mise"
|
|
256
|
+
command_tokens = unwrap_mise_exec(command_tokens)
|
|
257
|
+
when "env", "/usr/bin/env"
|
|
258
|
+
command_tokens = unwrap_env(command_tokens)
|
|
259
|
+
when "bundle"
|
|
260
|
+
return File.basename(command_tokens.first, ".rb") unless command_tokens[1] == "exec"
|
|
261
|
+
|
|
262
|
+
command_tokens = command_tokens.drop(2)
|
|
263
|
+
when "ruby"
|
|
264
|
+
return File.basename(command_tokens[1], ".rb") if command_tokens[1]
|
|
265
|
+
|
|
266
|
+
command_tokens = []
|
|
267
|
+
else
|
|
268
|
+
return File.basename(command_tokens.first, ".rb")
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def strip_env_assignments(tokens)
|
|
274
|
+
tokens.drop_while { |token| token.include?("=") && !token.start_with?("/", "./", "../") }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def unwrap_mise_exec(tokens)
|
|
278
|
+
return tokens unless tokens[1] == "exec"
|
|
279
|
+
|
|
280
|
+
separator_index = tokens.index("--")
|
|
281
|
+
remaining = separator_index ? tokens.drop(separator_index + 1) : tokens.drop(2)
|
|
282
|
+
strip_env_assignments(remaining)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def unwrap_env(tokens)
|
|
286
|
+
remaining = tokens.drop(1)
|
|
287
|
+
remaining = remaining.drop_while { |token| token.start_with?("-") }
|
|
288
|
+
strip_env_assignments(remaining)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def humanize(value)
|
|
292
|
+
value.to_s.split(/[_\s]+/).map(&:capitalize).join(" ")
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def bullet_line(value)
|
|
296
|
+
"- #{value}"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|