aidp 0.24.0 → 0.25.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -1
  3. data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
  4. data/lib/aidp/auto_update/checkpoint.rb +178 -0
  5. data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
  6. data/lib/aidp/auto_update/coordinator.rb +204 -0
  7. data/lib/aidp/auto_update/errors.rb +17 -0
  8. data/lib/aidp/auto_update/failure_tracker.rb +162 -0
  9. data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
  10. data/lib/aidp/auto_update/update_check.rb +106 -0
  11. data/lib/aidp/auto_update/update_logger.rb +143 -0
  12. data/lib/aidp/auto_update/update_policy.rb +109 -0
  13. data/lib/aidp/auto_update/version_detector.rb +144 -0
  14. data/lib/aidp/auto_update.rb +52 -0
  15. data/lib/aidp/cli.rb +165 -1
  16. data/lib/aidp/harness/config_schema.rb +50 -0
  17. data/lib/aidp/harness/provider_factory.rb +2 -0
  18. data/lib/aidp/message_display.rb +10 -2
  19. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
  20. data/lib/aidp/provider_manager.rb +2 -0
  21. data/lib/aidp/providers/kilocode.rb +202 -0
  22. data/lib/aidp/setup/provider_registry.rb +15 -0
  23. data/lib/aidp/setup/wizard.rb +12 -4
  24. data/lib/aidp/skills/composer.rb +4 -0
  25. data/lib/aidp/skills/loader.rb +3 -1
  26. data/lib/aidp/version.rb +1 -1
  27. data/lib/aidp/watch/build_processor.rb +66 -16
  28. data/lib/aidp/watch/ci_fix_processor.rb +448 -0
  29. data/lib/aidp/watch/plan_processor.rb +12 -2
  30. data/lib/aidp/watch/repository_client.rb +380 -0
  31. data/lib/aidp/watch/review_processor.rb +266 -0
  32. data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
  33. data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
  34. data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
  35. data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
  36. data/lib/aidp/watch/runner.rb +185 -0
  37. data/lib/aidp/watch/state_store.rb +53 -0
  38. data/lib/aidp.rb +1 -0
  39. metadata +20 -1
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+ require_relative "base"
5
+ require_relative "../util"
6
+ require_relative "../debug_mixin"
7
+
8
+ module Aidp
9
+ module Providers
10
+ class Kilocode < Base
11
+ include Aidp::DebugMixin
12
+
13
+ def self.available?
14
+ !!Aidp::Util.which("kilocode")
15
+ end
16
+
17
+ def name
18
+ "kilocode"
19
+ end
20
+
21
+ def display_name
22
+ "Kilocode"
23
+ end
24
+
25
+ def send_message(prompt:, session: nil)
26
+ raise "kilocode not available" unless self.class.available?
27
+
28
+ # Smart timeout calculation
29
+ timeout_seconds = calculate_timeout
30
+
31
+ debug_provider("kilocode", "Starting execution", {timeout: timeout_seconds})
32
+ debug_log("📝 Sending prompt to kilocode (length: #{prompt.length})", level: :info)
33
+
34
+ # Check if streaming mode is enabled
35
+ streaming_enabled = ENV["AIDP_STREAMING"] == "1" || ENV["DEBUG"] == "1"
36
+ if streaming_enabled
37
+ display_message("📺 Display streaming enabled - output buffering reduced", type: :info)
38
+ end
39
+
40
+ # Check if prompt is too large and warn
41
+ if prompt.length > 3000
42
+ debug_log("⚠️ Large prompt detected (#{prompt.length} chars) - this may cause rate limiting", level: :warn)
43
+ end
44
+
45
+ # Set up activity monitoring
46
+ setup_activity_monitoring("kilocode", method(:activity_callback))
47
+ record_activity("Starting kilocode execution")
48
+
49
+ # Create a spinner for activity display
50
+ spinner = TTY::Spinner.new("[:spinner] :title", format: :dots, hide_cursor: true)
51
+ spinner.auto_spin
52
+
53
+ activity_display_thread = Thread.new do
54
+ start_time = Time.now
55
+ loop do
56
+ sleep 0.5 # Update every 500ms to reduce spam
57
+ elapsed = Time.now - start_time
58
+
59
+ # Break if we've been running too long or state changed
60
+ break if elapsed > timeout_seconds || @activity_state == :completed || @activity_state == :failed
61
+
62
+ update_spinner_status(spinner, elapsed, "🔄 kilocode")
63
+ end
64
+ end
65
+
66
+ begin
67
+ # Build kilocode command arguments
68
+ args = ["--auto"]
69
+
70
+ # Add model if specified
71
+ model = ENV["KILOCODE_MODEL"]
72
+ if model
73
+ args.concat(["-m", model])
74
+ end
75
+
76
+ # Add workspace detection if needed
77
+ if Dir.exist?(".git") && ENV["KILOCODE_WORKSPACE"]
78
+ args.concat(["--workspace", ENV["KILOCODE_WORKSPACE"]])
79
+ end
80
+
81
+ # Set authentication via environment variable
82
+ env_vars = {}
83
+ if ENV["KILOCODE_TOKEN"]
84
+ env_vars["KILOCODE_TOKEN"] = ENV["KILOCODE_TOKEN"]
85
+ end
86
+
87
+ # Use debug_execute_command for better debugging
88
+ result = debug_execute_command("kilocode", args: args, input: prompt, timeout: timeout_seconds, streaming: streaming_enabled, env: env_vars)
89
+
90
+ # Log the results
91
+ debug_command("kilocode", args: args, input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
92
+
93
+ if result.exit_status == 0
94
+ spinner.success("✓")
95
+ mark_completed
96
+ result.out
97
+ else
98
+ spinner.error("✗")
99
+ mark_failed("kilocode failed with exit code #{result.exit_status}")
100
+ debug_error(StandardError.new("kilocode failed"), {exit_code: result.exit_status, stderr: result.err})
101
+ raise "kilocode failed with exit code #{result.exit_status}: #{result.err}"
102
+ end
103
+ rescue => e
104
+ spinner&.error("✗")
105
+ mark_failed("kilocode execution failed: #{e.message}")
106
+ debug_error(e, {provider: "kilocode", prompt_length: prompt.length})
107
+ raise
108
+ ensure
109
+ cleanup_activity_display(activity_display_thread, spinner)
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def calculate_timeout
116
+ # Priority order for timeout calculation:
117
+ # 1. Quick mode (for testing)
118
+ # 2. Environment variable override
119
+ # 3. Adaptive timeout based on step type
120
+ # 4. Default timeout
121
+
122
+ if ENV["AIDP_QUICK_MODE"]
123
+ display_message("⚡ Quick mode enabled - #{TIMEOUT_QUICK_MODE / 60} minute timeout", type: :highlight)
124
+ return TIMEOUT_QUICK_MODE
125
+ end
126
+
127
+ if ENV["AIDP_KILOCODE_TIMEOUT"]
128
+ return ENV["AIDP_KILOCODE_TIMEOUT"].to_i
129
+ end
130
+
131
+ if adaptive_timeout
132
+ display_message("🧠 Using adaptive timeout: #{adaptive_timeout} seconds", type: :info)
133
+ return adaptive_timeout
134
+ end
135
+
136
+ # Default timeout
137
+ display_message("📋 Using default timeout: #{TIMEOUT_DEFAULT / 60} minutes", type: :info)
138
+ TIMEOUT_DEFAULT
139
+ end
140
+
141
+ def adaptive_timeout
142
+ @adaptive_timeout ||= begin
143
+ # Timeout recommendations based on step type patterns
144
+ step_name = ENV["AIDP_CURRENT_STEP"] || ""
145
+
146
+ case step_name
147
+ when /REPOSITORY_ANALYSIS/
148
+ TIMEOUT_REPOSITORY_ANALYSIS
149
+ when /ARCHITECTURE_ANALYSIS/
150
+ TIMEOUT_ARCHITECTURE_ANALYSIS
151
+ when /TEST_ANALYSIS/
152
+ TIMEOUT_TEST_ANALYSIS
153
+ when /FUNCTIONALITY_ANALYSIS/
154
+ TIMEOUT_FUNCTIONALITY_ANALYSIS
155
+ when /DOCUMENTATION_ANALYSIS/
156
+ TIMEOUT_DOCUMENTATION_ANALYSIS
157
+ when /STATIC_ANALYSIS/
158
+ TIMEOUT_STATIC_ANALYSIS
159
+ when /REFACTORING_RECOMMENDATIONS/
160
+ TIMEOUT_REFACTORING_RECOMMENDATIONS
161
+ else
162
+ nil # Use default
163
+ end
164
+ end
165
+ end
166
+
167
+ def activity_callback(state, message, provider)
168
+ # This is now handled by the animated display thread
169
+ # Only print static messages for state changes
170
+ case state
171
+ when :starting
172
+ display_message("🚀 Starting kilocode execution...", type: :info)
173
+ when :completed
174
+ display_message("✅ kilocode execution completed", type: :success)
175
+ when :failed
176
+ display_message("❌ kilocode execution failed: #{message}", type: :error)
177
+ end
178
+ end
179
+
180
+ def setup_activity_monitoring(provider_name, callback)
181
+ @activity_callback = callback
182
+ @activity_state = :starting
183
+ @activity_start_time = Time.now
184
+ end
185
+
186
+ def record_activity(message)
187
+ @activity_state = :running
188
+ @activity_callback&.call(:running, message, "kilocode")
189
+ end
190
+
191
+ def mark_completed
192
+ @activity_state = :completed
193
+ @activity_callback&.call(:completed, "Execution completed", "kilocode")
194
+ end
195
+
196
+ def mark_failed(reason)
197
+ @activity_state = :failed
198
+ @activity_callback&.call(:failed, reason, "kilocode")
199
+ end
200
+ end
201
+ end
202
+ end
@@ -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",
@@ -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
- # Already a canonical value
1309
- return value if ProviderRegistry.valid_model_family?(value)
1310
- # Try label -> value
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
@@ -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
 
@@ -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 == "---"
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.24.0"
4
+ VERSION = "0.25.0"
5
5
  end
@@ -355,7 +355,7 @@ module Aidp
355
355
  FileUtils.mkdir_p(File.dirname(target_config))
356
356
 
357
357
  # Only copy when target missing or differs
358
- if !File.exist?(target_config) || File.read(source_config) != File.read(target_config)
358
+ if !File.exist?(target_config) || File.read(source_config, encoding: "UTF-8") != File.read(target_config, encoding: "UTF-8")
359
359
  FileUtils.cp(source_config, target_config)
360
360
  end
361
361
  rescue => e
@@ -382,8 +382,8 @@ module Aidp
382
382
 
383
383
  # Check if PR should be created based on VCS preferences
384
384
  # For watch mode, default to creating PRs (set to false to disable)
385
- vcs_config = config.dig(:work_loop, :version_control) || {}
386
- auto_create_pr = vcs_config.fetch(:auto_create_pr, true)
385
+ vcs_config = config_dig(:work_loop, :version_control) || {}
386
+ auto_create_pr = config_value(vcs_config, :auto_create_pr, true)
387
387
 
388
388
  pr_url = if !changes_committed
389
389
  Aidp.log_info(
@@ -410,12 +410,17 @@ module Aidp
410
410
  nil
411
411
  end
412
412
 
413
+ # Fetch the user who added the most recent label
414
+ label_actor = @repository_client.most_recent_label_actor(issue[:number])
415
+
413
416
  workstream_note = @use_workstreams ? "\n- Workstream: `#{slug}`" : ""
414
417
  pr_line = pr_url ? "\n- Pull Request: #{pr_url}" : ""
418
+ actor_tag = label_actor ? "cc @#{label_actor}\n\n" : ""
415
419
 
416
420
  comment = <<~COMMENT
417
421
  ✅ Implementation complete for ##{issue[:number]}.
418
- - Branch: `#{branch_name}`#{workstream_note}#{pr_line}
422
+
423
+ #{actor_tag}- Branch: `#{branch_name}`#{workstream_note}#{pr_line}
419
424
 
420
425
  Summary:
421
426
  #{plan_value(plan_data, "summary")}
@@ -448,10 +453,20 @@ module Aidp
448
453
  questions = result[:clarification_questions] || []
449
454
  workstream_note = @use_workstreams ? " The workstream `#{slug}` has been preserved." : " The branch has been preserved."
450
455
 
456
+ # Fetch the user who added the most recent label
457
+ label_actor = @repository_client.most_recent_label_actor(issue[:number])
458
+
451
459
  # Build comment with questions
452
460
  comment_parts = []
453
461
  comment_parts << "❓ Implementation needs clarification for ##{issue[:number]}."
454
462
  comment_parts << ""
463
+
464
+ # Tag the label actor if available
465
+ if label_actor
466
+ comment_parts << "cc @#{label_actor}"
467
+ comment_parts << ""
468
+ end
469
+
455
470
  comment_parts << "The AI agent needs additional information to proceed with implementation:"
456
471
  comment_parts << ""
457
472
  questions.each_with_index do |question, index|
@@ -615,15 +630,15 @@ module Aidp
615
630
  end
616
631
 
617
632
  def build_commit_message(issue)
618
- vcs_config = config.dig(:work_loop, :version_control) || {}
633
+ vcs_config = config_dig(:work_loop, :version_control) || {}
619
634
 
620
635
  # Base message components
621
636
  issue_ref = "##{issue[:number]}"
622
637
  title = issue[:title]
623
638
 
624
639
  # Determine commit prefix based on configuration
625
- prefix = if vcs_config[:conventional_commits]
626
- commit_style = vcs_config[:commit_style] || "default"
640
+ prefix = if config_value(vcs_config, :conventional_commits)
641
+ commit_style = config_value(vcs_config, :commit_style, "default")
627
642
  emoji = (commit_style == "emoji") ? "✨ " : ""
628
643
  scope = (commit_style == "angular") ? "(implementation)" : ""
629
644
  "#{emoji}feat#{scope}: "
@@ -635,7 +650,7 @@ module Aidp
635
650
  main_message = "#{prefix}implement #{issue_ref} #{title}"
636
651
 
637
652
  # Add co-author attribution if configured
638
- if vcs_config.fetch(:co_author_ai, true)
653
+ if config_value(vcs_config, :co_author_ai, true)
639
654
  provider_name = detect_current_provider || "AI Agent"
640
655
  co_author = "\n\nCo-authored-by: #{provider_name} <ai@aidp.dev>"
641
656
  main_message + co_author
@@ -658,30 +673,64 @@ module Aidp
658
673
  @config ||= begin
659
674
  config_manager = Aidp::Harness::ConfigManager.new(@project_dir)
660
675
  config_manager.config || {}
661
- rescue
676
+ rescue => e
677
+ Aidp.log_error("build_processor", "config_load_exception", project_dir: @project_dir, error: e.message, backtrace: e.backtrace&.first(5))
662
678
  {}
663
679
  end
664
680
  end
665
681
 
682
+ # Helper to safely dig into config with both string and symbol keys
683
+ def config_dig(*keys)
684
+ value = config
685
+ keys.each do |key|
686
+ return nil unless value.is_a?(Hash)
687
+ # Try both symbol and string versions of the key
688
+ value = value[key] || value[key.to_s] || value[key.to_sym]
689
+ return nil if value.nil?
690
+ end
691
+ value
692
+ end
693
+
694
+ # Helper to get config value with both string and symbol key support
695
+ def config_value(hash, key, default = nil)
696
+ return default unless hash.is_a?(Hash)
697
+ # Check each key variation explicitly to handle false/nil values correctly
698
+ return hash[key] if hash.key?(key)
699
+ return hash[key.to_s] if hash.key?(key.to_s)
700
+ return hash[key.to_sym] if hash.key?(key.to_sym)
701
+ default
702
+ end
703
+
666
704
  def create_pull_request(issue:, branch_name:, base_branch:, working_dir: @project_dir)
667
705
  title = "aidp: Resolve ##{issue[:number]} - #{issue[:title]}"
668
706
  test_summary = gather_test_summary(working_dir: working_dir)
669
707
  body = <<~BODY
708
+ Fixes ##{issue[:number]}
709
+
670
710
  ## Summary
671
711
  - Automated resolution for ##{issue[:number]}
672
- - Fixes ##{issue[:number]}
673
712
 
674
713
  ## Testing
675
714
  #{test_summary}
676
715
  BODY
677
716
 
678
717
  # Determine if PR should be draft based on VCS preferences
679
- vcs_config = config.dig(:work_loop, :version_control) || {}
680
- pr_strategy = vcs_config[:pr_strategy] || "draft"
718
+ vcs_config = config_dig(:work_loop, :version_control) || {}
719
+ pr_strategy = config_value(vcs_config, :pr_strategy, "draft")
681
720
  draft = (pr_strategy == "draft")
682
721
 
683
- # Assign PR to the issue author
684
- assignee = issue[:author]
722
+ # Fetch the user who added the most recent label to assign the PR
723
+ label_actor = @repository_client.most_recent_label_actor(issue[:number])
724
+ assignee = label_actor || issue[:author]
725
+
726
+ Aidp.log_info(
727
+ "build_processor",
728
+ "assigning_pr",
729
+ issue: issue[:number],
730
+ assignee: assignee,
731
+ label_actor: label_actor,
732
+ fallback_to_author: label_actor.nil?
733
+ )
685
734
 
686
735
  output = @repository_client.create_pull_request(
687
736
  title: title,
@@ -700,7 +749,8 @@ module Aidp
700
749
  issue: issue[:number],
701
750
  branch: branch_name,
702
751
  base_branch: base_branch,
703
- pr_url: pr_url
752
+ pr_url: pr_url,
753
+ assignee: assignee
704
754
  )
705
755
  pr_url
706
756
  end
@@ -710,7 +760,7 @@ module Aidp
710
760
  log_path = File.join(".aidp", "logs", "test_runner.log")
711
761
  return "- Fix-forward harness executed; refer to #{log_path}" unless File.exist?(log_path)
712
762
 
713
- recent = File.readlines(log_path).last(20).map(&:strip).reject(&:empty?)
763
+ recent = File.readlines(log_path, encoding: "UTF-8").last(20).map(&:strip).reject(&:empty?)
714
764
  if recent.empty?
715
765
  "- Fix-forward harness executed successfully."
716
766
  else