aidp 0.24.0 → 0.26.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 +72 -7
- data/lib/aidp/analyze/error_handler.rb +11 -0
- data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
- data/lib/aidp/auto_update/checkpoint.rb +178 -0
- data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
- data/lib/aidp/auto_update/coordinator.rb +204 -0
- data/lib/aidp/auto_update/errors.rb +17 -0
- data/lib/aidp/auto_update/failure_tracker.rb +162 -0
- data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
- data/lib/aidp/auto_update/update_check.rb +106 -0
- data/lib/aidp/auto_update/update_logger.rb +143 -0
- data/lib/aidp/auto_update/update_policy.rb +109 -0
- data/lib/aidp/auto_update/version_detector.rb +144 -0
- data/lib/aidp/auto_update.rb +52 -0
- data/lib/aidp/cli.rb +165 -1
- data/lib/aidp/execute/work_loop_runner.rb +225 -55
- data/lib/aidp/harness/config_loader.rb +20 -11
- data/lib/aidp/harness/config_schema.rb +80 -8
- data/lib/aidp/harness/configuration.rb +73 -2
- data/lib/aidp/harness/filter_strategy.rb +45 -0
- data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
- data/lib/aidp/harness/output_filter.rb +136 -0
- data/lib/aidp/harness/provider_factory.rb +2 -0
- data/lib/aidp/harness/provider_manager.rb +18 -3
- data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
- data/lib/aidp/harness/test_runner.rb +165 -27
- data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
- data/lib/aidp/logger.rb +35 -5
- data/lib/aidp/message_display.rb +56 -2
- data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
- data/lib/aidp/provider_manager.rb +2 -0
- data/lib/aidp/providers/kilocode.rb +202 -0
- data/lib/aidp/safe_directory.rb +10 -3
- data/lib/aidp/setup/provider_registry.rb +15 -0
- data/lib/aidp/setup/wizard.rb +12 -4
- data/lib/aidp/skills/composer.rb +4 -0
- data/lib/aidp/skills/loader.rb +3 -1
- data/lib/aidp/storage/csv_storage.rb +9 -3
- data/lib/aidp/storage/file_manager.rb +8 -2
- data/lib/aidp/storage/json_storage.rb +9 -3
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +106 -17
- data/lib/aidp/watch/change_request_processor.rb +659 -0
- data/lib/aidp/watch/ci_fix_processor.rb +448 -0
- data/lib/aidp/watch/plan_processor.rb +81 -8
- data/lib/aidp/watch/repository_client.rb +465 -20
- data/lib/aidp/watch/review_processor.rb +266 -0
- data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
- data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
- data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
- data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
- data/lib/aidp/watch/runner.rb +222 -0
- data/lib/aidp/watch/state_store.rb +99 -1
- data/lib/aidp/workstream_executor.rb +5 -2
- data/lib/aidp.rb +5 -0
- data/templates/aidp.yml.example +53 -0
- metadata +25 -1
data/lib/aidp/safe_directory.rb
CHANGED
|
@@ -22,14 +22,21 @@ module Aidp
|
|
|
22
22
|
path
|
|
23
23
|
rescue SystemCallError => e
|
|
24
24
|
fallback = determine_fallback_path(path)
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
|
|
26
|
+
# Suppress permission warnings during tests to reduce noise
|
|
27
|
+
unless ENV["RSPEC_RUNNING"] == "true"
|
|
28
|
+
Kernel.warn "[#{component_name}] Cannot create directory #{path}: #{e.class}: #{e.message}"
|
|
29
|
+
Kernel.warn "[#{component_name}] Using fallback directory: #{fallback}"
|
|
30
|
+
end
|
|
27
31
|
|
|
28
32
|
# Try to create fallback directory
|
|
29
33
|
begin
|
|
30
34
|
FileUtils.mkdir_p(fallback) unless Dir.exist?(fallback)
|
|
31
35
|
rescue SystemCallError => e2
|
|
32
|
-
|
|
36
|
+
# Suppress fallback errors during tests too
|
|
37
|
+
unless ENV["RSPEC_RUNNING"] == "true"
|
|
38
|
+
Kernel.warn "[#{component_name}] Fallback directory creation also failed: #{e2.class}: #{e2.message}"
|
|
39
|
+
end
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
fallback
|
|
@@ -41,6 +41,21 @@ module Aidp
|
|
|
41
41
|
value: "claude",
|
|
42
42
|
description: "Balanced performance for general-purpose tasks"
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
label: "Google Gemini (multimodal)",
|
|
46
|
+
value: "gemini",
|
|
47
|
+
description: "Google's multimodal AI with strong reasoning and vision capabilities"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: "Meta Llama (open-source)",
|
|
51
|
+
value: "llama",
|
|
52
|
+
description: "Meta's open-source model family, suitable for self-hosting"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
label: "DeepSeek (efficient reasoning)",
|
|
56
|
+
value: "deepseek",
|
|
57
|
+
description: "Cost-efficient reasoning models with strong performance"
|
|
58
|
+
},
|
|
44
59
|
{
|
|
45
60
|
label: "Mistral (European/open)",
|
|
46
61
|
value: "mistral",
|
data/lib/aidp/setup/wizard.rb
CHANGED
|
@@ -1305,12 +1305,20 @@ module Aidp
|
|
|
1305
1305
|
# Canonicalization helpers ------------------------------------------------
|
|
1306
1306
|
def normalize_model_family(value)
|
|
1307
1307
|
return "auto" if value.nil? || value.to_s.strip.empty?
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1308
|
+
|
|
1309
|
+
normalized_input = value.to_s.strip.downcase
|
|
1310
|
+
|
|
1311
|
+
# Check for exact canonical value match (case-insensitive)
|
|
1312
|
+
canonical_match = ProviderRegistry.model_family_values.find do |v|
|
|
1313
|
+
v.downcase == normalized_input
|
|
1314
|
+
end
|
|
1315
|
+
return canonical_match if canonical_match
|
|
1316
|
+
|
|
1317
|
+
# Try label -> value mapping (case-insensitive)
|
|
1311
1318
|
choices = ProviderRegistry.model_family_choices
|
|
1312
|
-
mapped = choices.find { |label, _| label == value }&.last
|
|
1319
|
+
mapped = choices.find { |label, _| label.downcase == value.to_s.downcase }&.last
|
|
1313
1320
|
return mapped if mapped
|
|
1321
|
+
|
|
1314
1322
|
# Unknown legacy entry -> fallback to auto
|
|
1315
1323
|
"auto"
|
|
1316
1324
|
end
|
data/lib/aidp/skills/composer.rb
CHANGED
|
@@ -82,6 +82,8 @@ module Aidp
|
|
|
82
82
|
def render_template(template, options: {})
|
|
83
83
|
return template if options.empty?
|
|
84
84
|
|
|
85
|
+
# Ensure template is UTF-8 encoded
|
|
86
|
+
template = template.encode("UTF-8", invalid: :replace, undef: :replace) unless template.encoding == Encoding::UTF_8
|
|
85
87
|
rendered = template.dup
|
|
86
88
|
|
|
87
89
|
options.each do |key, value|
|
|
@@ -158,6 +160,8 @@ module Aidp
|
|
|
158
160
|
def extract_placeholders(text)
|
|
159
161
|
return [] if text.nil? || text.empty?
|
|
160
162
|
|
|
163
|
+
# Ensure text is UTF-8 encoded
|
|
164
|
+
text = text.encode("UTF-8", invalid: :replace, undef: :replace) unless text.encoding == Encoding::UTF_8
|
|
161
165
|
scanner = StringScanner.new(text)
|
|
162
166
|
placeholders = []
|
|
163
167
|
|
data/lib/aidp/skills/loader.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Aidp
|
|
|
34
34
|
raise Aidp::Errors::ValidationError, "Skill file not found: #{file_path}"
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
content = File.read(file_path)
|
|
37
|
+
content = File.read(file_path, encoding: "UTF-8")
|
|
38
38
|
load_from_string(content, source_path: file_path, provider: provider)
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -139,6 +139,8 @@ module Aidp
|
|
|
139
139
|
# @return [Array(Hash, String)] Tuple of [metadata, markdown_content]
|
|
140
140
|
# @raise [Aidp::Errors::ValidationError] if frontmatter is missing or invalid
|
|
141
141
|
def self.parse_frontmatter(content, source_path:)
|
|
142
|
+
# Ensure content is UTF-8 encoded
|
|
143
|
+
content = content.encode("UTF-8", invalid: :replace, undef: :replace) unless content.encoding == Encoding::UTF_8
|
|
142
144
|
lines = content.lines
|
|
143
145
|
|
|
144
146
|
unless lines.first&.strip == "---"
|
|
@@ -205,12 +205,12 @@ module Aidp
|
|
|
205
205
|
rescue
|
|
206
206
|
File.join(Dir.tmpdir, "aidp_storage")
|
|
207
207
|
end
|
|
208
|
-
|
|
208
|
+
warn_storage("[AIDP Storage] Cannot create base directory #{@base_dir}: #{e.class}: #{e.message}. Using fallback #{fallback}")
|
|
209
209
|
@base_dir = fallback
|
|
210
210
|
begin
|
|
211
211
|
FileUtils.mkdir_p(@base_dir) unless Dir.exist?(@base_dir)
|
|
212
212
|
rescue SystemCallError => e2
|
|
213
|
-
|
|
213
|
+
warn_storage("[AIDP Storage] Fallback directory creation also failed: #{e2.class}: #{e2.message}. Continuing without persistent CSV storage.")
|
|
214
214
|
end
|
|
215
215
|
end
|
|
216
216
|
end
|
|
@@ -225,11 +225,17 @@ module Aidp
|
|
|
225
225
|
rescue
|
|
226
226
|
File.join(Dir.tmpdir, "aidp_storage")
|
|
227
227
|
end
|
|
228
|
-
|
|
228
|
+
warn_storage("[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'")
|
|
229
229
|
return fallback
|
|
230
230
|
end
|
|
231
231
|
str
|
|
232
232
|
end
|
|
233
|
+
|
|
234
|
+
# Suppress storage warnings in test/CI environments
|
|
235
|
+
def warn_storage(message)
|
|
236
|
+
return if ENV["RSPEC_RUNNING"] || ENV["CI"] || ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
|
|
237
|
+
Kernel.warn(message)
|
|
238
|
+
end
|
|
233
239
|
end
|
|
234
240
|
end
|
|
235
241
|
end
|
|
@@ -16,7 +16,7 @@ module Aidp
|
|
|
16
16
|
csv_dir = @csv_storage.instance_variable_get(:@base_dir)
|
|
17
17
|
if json_dir != @base_dir || csv_dir != @base_dir
|
|
18
18
|
@base_dir = json_dir # Prefer JSON storage directory
|
|
19
|
-
|
|
19
|
+
warn_storage("[AIDP Storage] Base directory normalized to #{@base_dir} after fallback.")
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -229,11 +229,17 @@ module Aidp
|
|
|
229
229
|
rescue
|
|
230
230
|
File.join(Dir.tmpdir, "aidp_storage")
|
|
231
231
|
end
|
|
232
|
-
|
|
232
|
+
warn_storage("[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'")
|
|
233
233
|
return fallback
|
|
234
234
|
end
|
|
235
235
|
str
|
|
236
236
|
end
|
|
237
|
+
|
|
238
|
+
# Suppress storage warnings in test/CI environments
|
|
239
|
+
def warn_storage(message)
|
|
240
|
+
return if ENV["RSPEC_RUNNING"] || ENV["CI"] || ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
|
|
241
|
+
Kernel.warn(message)
|
|
242
|
+
end
|
|
237
243
|
end
|
|
238
244
|
end
|
|
239
245
|
end
|
|
@@ -171,12 +171,12 @@ module Aidp
|
|
|
171
171
|
rescue
|
|
172
172
|
File.join(Dir.tmpdir, "aidp_storage")
|
|
173
173
|
end
|
|
174
|
-
|
|
174
|
+
warn_storage("[AIDP Storage] Cannot create base directory #{@base_dir}: #{e.class}: #{e.message}. Using fallback #{fallback}")
|
|
175
175
|
@base_dir = fallback
|
|
176
176
|
begin
|
|
177
177
|
FileUtils.mkdir_p(@base_dir) unless Dir.exist?(@base_dir)
|
|
178
178
|
rescue SystemCallError => e2
|
|
179
|
-
|
|
179
|
+
warn_storage("[AIDP Storage] Fallback directory creation also failed: #{e2.class}: #{e2.message}. Continuing without persistent JSON storage.")
|
|
180
180
|
end
|
|
181
181
|
end
|
|
182
182
|
end
|
|
@@ -192,11 +192,17 @@ module Aidp
|
|
|
192
192
|
rescue
|
|
193
193
|
File.join(Dir.tmpdir, "aidp_storage")
|
|
194
194
|
end
|
|
195
|
-
|
|
195
|
+
warn_storage("[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'")
|
|
196
196
|
return fallback
|
|
197
197
|
end
|
|
198
198
|
str
|
|
199
199
|
end
|
|
200
|
+
|
|
201
|
+
# Suppress storage warnings in test/CI environments
|
|
202
|
+
def warn_storage(message)
|
|
203
|
+
return if ENV["RSPEC_RUNNING"] || ENV["CI"] || ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
|
|
204
|
+
Kernel.warn(message)
|
|
205
|
+
end
|
|
200
206
|
end
|
|
201
207
|
end
|
|
202
208
|
end
|
data/lib/aidp/version.rb
CHANGED
|
@@ -228,12 +228,51 @@ module Aidp
|
|
|
228
228
|
relevant.map do |comment|
|
|
229
229
|
author = comment["author"] || "unknown"
|
|
230
230
|
created = comment["createdAt"] ? Time.parse(comment["createdAt"]).utc.iso8601 : "unknown"
|
|
231
|
-
|
|
231
|
+
body = strip_archived_plans(comment["body"])
|
|
232
|
+
"### #{author} (#{created})\n#{body}"
|
|
232
233
|
end.join("\n\n")
|
|
233
234
|
rescue
|
|
234
235
|
"_Unable to parse comment thread._"
|
|
235
236
|
end
|
|
236
237
|
|
|
238
|
+
def strip_archived_plans(content)
|
|
239
|
+
return content unless content
|
|
240
|
+
|
|
241
|
+
# Remove all archived plan sections (wrapped in HTML comments)
|
|
242
|
+
result = content.dup
|
|
243
|
+
|
|
244
|
+
# Remove archived plan blocks
|
|
245
|
+
# Safe string-based approach to avoid ReDoS vulnerabilities
|
|
246
|
+
start_prefix = "<!-- ARCHIVED_PLAN_START"
|
|
247
|
+
end_marker = "<!-- ARCHIVED_PLAN_END -->"
|
|
248
|
+
|
|
249
|
+
loop do
|
|
250
|
+
# Find the start of an archived plan block (may have attributes after ARCHIVED_PLAN_START)
|
|
251
|
+
start_idx = result.index(start_prefix)
|
|
252
|
+
break unless start_idx
|
|
253
|
+
|
|
254
|
+
# Find the closing --> of the start marker
|
|
255
|
+
start_marker_end = result.index("-->", start_idx)
|
|
256
|
+
break unless start_marker_end
|
|
257
|
+
|
|
258
|
+
# Find the corresponding end marker
|
|
259
|
+
end_idx = result.index(end_marker, start_marker_end)
|
|
260
|
+
break unless end_idx
|
|
261
|
+
|
|
262
|
+
# Remove the entire block including markers
|
|
263
|
+
result = result[0...start_idx] + result[(end_idx + end_marker.length)..]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Remove HTML-commented sections from active plan
|
|
267
|
+
# Keep the content between START and END markers, but strip the markers themselves
|
|
268
|
+
# This preserves the current plan while removing archived content
|
|
269
|
+
result = result.gsub(/<!-- (PLAN_SUMMARY_START|PLAN_TASKS_START|CLARIFYING_QUESTIONS_START) -->/, "")
|
|
270
|
+
result = result.gsub(/<!-- (PLAN_SUMMARY_END|PLAN_TASKS_END|CLARIFYING_QUESTIONS_END) -->/, "")
|
|
271
|
+
|
|
272
|
+
# Clean up any extra blank lines
|
|
273
|
+
result.gsub(/\n{3,}/, "\n\n").strip
|
|
274
|
+
end
|
|
275
|
+
|
|
237
276
|
def write_prompt(content, working_dir: @project_dir)
|
|
238
277
|
prompt_manager = Aidp::Execute::PromptManager.new(working_dir)
|
|
239
278
|
prompt_manager.write(content, step_name: IMPLEMENTATION_STEP)
|
|
@@ -355,7 +394,7 @@ module Aidp
|
|
|
355
394
|
FileUtils.mkdir_p(File.dirname(target_config))
|
|
356
395
|
|
|
357
396
|
# Only copy when target missing or differs
|
|
358
|
-
if !File.exist?(target_config) || File.read(source_config) != File.read(target_config)
|
|
397
|
+
if !File.exist?(target_config) || File.read(source_config, encoding: "UTF-8") != File.read(target_config, encoding: "UTF-8")
|
|
359
398
|
FileUtils.cp(source_config, target_config)
|
|
360
399
|
end
|
|
361
400
|
rescue => e
|
|
@@ -382,8 +421,8 @@ module Aidp
|
|
|
382
421
|
|
|
383
422
|
# Check if PR should be created based on VCS preferences
|
|
384
423
|
# For watch mode, default to creating PRs (set to false to disable)
|
|
385
|
-
vcs_config =
|
|
386
|
-
auto_create_pr = vcs_config
|
|
424
|
+
vcs_config = config_dig(:work_loop, :version_control) || {}
|
|
425
|
+
auto_create_pr = config_value(vcs_config, :auto_create_pr, true)
|
|
387
426
|
|
|
388
427
|
pr_url = if !changes_committed
|
|
389
428
|
Aidp.log_info(
|
|
@@ -410,12 +449,17 @@ module Aidp
|
|
|
410
449
|
nil
|
|
411
450
|
end
|
|
412
451
|
|
|
452
|
+
# Fetch the user who added the most recent label
|
|
453
|
+
label_actor = @repository_client.most_recent_label_actor(issue[:number])
|
|
454
|
+
|
|
413
455
|
workstream_note = @use_workstreams ? "\n- Workstream: `#{slug}`" : ""
|
|
414
456
|
pr_line = pr_url ? "\n- Pull Request: #{pr_url}" : ""
|
|
457
|
+
actor_tag = label_actor ? "cc @#{label_actor}\n\n" : ""
|
|
415
458
|
|
|
416
459
|
comment = <<~COMMENT
|
|
417
460
|
✅ Implementation complete for ##{issue[:number]}.
|
|
418
|
-
|
|
461
|
+
|
|
462
|
+
#{actor_tag}- Branch: `#{branch_name}`#{workstream_note}#{pr_line}
|
|
419
463
|
|
|
420
464
|
Summary:
|
|
421
465
|
#{plan_value(plan_data, "summary")}
|
|
@@ -448,10 +492,20 @@ module Aidp
|
|
|
448
492
|
questions = result[:clarification_questions] || []
|
|
449
493
|
workstream_note = @use_workstreams ? " The workstream `#{slug}` has been preserved." : " The branch has been preserved."
|
|
450
494
|
|
|
495
|
+
# Fetch the user who added the most recent label
|
|
496
|
+
label_actor = @repository_client.most_recent_label_actor(issue[:number])
|
|
497
|
+
|
|
451
498
|
# Build comment with questions
|
|
452
499
|
comment_parts = []
|
|
453
500
|
comment_parts << "❓ Implementation needs clarification for ##{issue[:number]}."
|
|
454
501
|
comment_parts << ""
|
|
502
|
+
|
|
503
|
+
# Tag the label actor if available
|
|
504
|
+
if label_actor
|
|
505
|
+
comment_parts << "cc @#{label_actor}"
|
|
506
|
+
comment_parts << ""
|
|
507
|
+
end
|
|
508
|
+
|
|
455
509
|
comment_parts << "The AI agent needs additional information to proceed with implementation:"
|
|
456
510
|
comment_parts << ""
|
|
457
511
|
questions.each_with_index do |question, index|
|
|
@@ -615,15 +669,15 @@ module Aidp
|
|
|
615
669
|
end
|
|
616
670
|
|
|
617
671
|
def build_commit_message(issue)
|
|
618
|
-
vcs_config =
|
|
672
|
+
vcs_config = config_dig(:work_loop, :version_control) || {}
|
|
619
673
|
|
|
620
674
|
# Base message components
|
|
621
675
|
issue_ref = "##{issue[:number]}"
|
|
622
676
|
title = issue[:title]
|
|
623
677
|
|
|
624
678
|
# Determine commit prefix based on configuration
|
|
625
|
-
prefix = if vcs_config
|
|
626
|
-
commit_style = vcs_config
|
|
679
|
+
prefix = if config_value(vcs_config, :conventional_commits)
|
|
680
|
+
commit_style = config_value(vcs_config, :commit_style, "default")
|
|
627
681
|
emoji = (commit_style == "emoji") ? "✨ " : ""
|
|
628
682
|
scope = (commit_style == "angular") ? "(implementation)" : ""
|
|
629
683
|
"#{emoji}feat#{scope}: "
|
|
@@ -635,7 +689,7 @@ module Aidp
|
|
|
635
689
|
main_message = "#{prefix}implement #{issue_ref} #{title}"
|
|
636
690
|
|
|
637
691
|
# Add co-author attribution if configured
|
|
638
|
-
if vcs_config
|
|
692
|
+
if config_value(vcs_config, :co_author_ai, true)
|
|
639
693
|
provider_name = detect_current_provider || "AI Agent"
|
|
640
694
|
co_author = "\n\nCo-authored-by: #{provider_name} <ai@aidp.dev>"
|
|
641
695
|
main_message + co_author
|
|
@@ -658,30 +712,64 @@ module Aidp
|
|
|
658
712
|
@config ||= begin
|
|
659
713
|
config_manager = Aidp::Harness::ConfigManager.new(@project_dir)
|
|
660
714
|
config_manager.config || {}
|
|
661
|
-
rescue
|
|
715
|
+
rescue => e
|
|
716
|
+
Aidp.log_error("build_processor", "config_load_exception", project_dir: @project_dir, error: e.message, backtrace: e.backtrace&.first(5))
|
|
662
717
|
{}
|
|
663
718
|
end
|
|
664
719
|
end
|
|
665
720
|
|
|
721
|
+
# Helper to safely dig into config with both string and symbol keys
|
|
722
|
+
def config_dig(*keys)
|
|
723
|
+
value = config
|
|
724
|
+
keys.each do |key|
|
|
725
|
+
return nil unless value.is_a?(Hash)
|
|
726
|
+
# Try both symbol and string versions of the key
|
|
727
|
+
value = value[key] || value[key.to_s] || value[key.to_sym]
|
|
728
|
+
return nil if value.nil?
|
|
729
|
+
end
|
|
730
|
+
value
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# Helper to get config value with both string and symbol key support
|
|
734
|
+
def config_value(hash, key, default = nil)
|
|
735
|
+
return default unless hash.is_a?(Hash)
|
|
736
|
+
# Check each key variation explicitly to handle false/nil values correctly
|
|
737
|
+
return hash[key] if hash.key?(key)
|
|
738
|
+
return hash[key.to_s] if hash.key?(key.to_s)
|
|
739
|
+
return hash[key.to_sym] if hash.key?(key.to_sym)
|
|
740
|
+
default
|
|
741
|
+
end
|
|
742
|
+
|
|
666
743
|
def create_pull_request(issue:, branch_name:, base_branch:, working_dir: @project_dir)
|
|
667
744
|
title = "aidp: Resolve ##{issue[:number]} - #{issue[:title]}"
|
|
668
745
|
test_summary = gather_test_summary(working_dir: working_dir)
|
|
669
746
|
body = <<~BODY
|
|
747
|
+
Fixes ##{issue[:number]}
|
|
748
|
+
|
|
670
749
|
## Summary
|
|
671
750
|
- Automated resolution for ##{issue[:number]}
|
|
672
|
-
- Fixes ##{issue[:number]}
|
|
673
751
|
|
|
674
752
|
## Testing
|
|
675
753
|
#{test_summary}
|
|
676
754
|
BODY
|
|
677
755
|
|
|
678
756
|
# Determine if PR should be draft based on VCS preferences
|
|
679
|
-
vcs_config =
|
|
680
|
-
pr_strategy = vcs_config
|
|
757
|
+
vcs_config = config_dig(:work_loop, :version_control) || {}
|
|
758
|
+
pr_strategy = config_value(vcs_config, :pr_strategy, "draft")
|
|
681
759
|
draft = (pr_strategy == "draft")
|
|
682
760
|
|
|
683
|
-
#
|
|
684
|
-
|
|
761
|
+
# Fetch the user who added the most recent label to assign the PR
|
|
762
|
+
label_actor = @repository_client.most_recent_label_actor(issue[:number])
|
|
763
|
+
assignee = label_actor || issue[:author]
|
|
764
|
+
|
|
765
|
+
Aidp.log_info(
|
|
766
|
+
"build_processor",
|
|
767
|
+
"assigning_pr",
|
|
768
|
+
issue: issue[:number],
|
|
769
|
+
assignee: assignee,
|
|
770
|
+
label_actor: label_actor,
|
|
771
|
+
fallback_to_author: label_actor.nil?
|
|
772
|
+
)
|
|
685
773
|
|
|
686
774
|
output = @repository_client.create_pull_request(
|
|
687
775
|
title: title,
|
|
@@ -700,7 +788,8 @@ module Aidp
|
|
|
700
788
|
issue: issue[:number],
|
|
701
789
|
branch: branch_name,
|
|
702
790
|
base_branch: base_branch,
|
|
703
|
-
pr_url: pr_url
|
|
791
|
+
pr_url: pr_url,
|
|
792
|
+
assignee: assignee
|
|
704
793
|
)
|
|
705
794
|
pr_url
|
|
706
795
|
end
|
|
@@ -710,7 +799,7 @@ module Aidp
|
|
|
710
799
|
log_path = File.join(".aidp", "logs", "test_runner.log")
|
|
711
800
|
return "- Fix-forward harness executed; refer to #{log_path}" unless File.exist?(log_path)
|
|
712
801
|
|
|
713
|
-
recent = File.readlines(log_path).last(20).map(&:strip).reject(&:empty?)
|
|
802
|
+
recent = File.readlines(log_path, encoding: "UTF-8").last(20).map(&:strip).reject(&:empty?)
|
|
714
803
|
if recent.empty?
|
|
715
804
|
"- Fix-forward harness executed successfully."
|
|
716
805
|
else
|