aidp 0.25.0 → 0.27.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -6
  3. data/lib/aidp/analyze/error_handler.rb +11 -0
  4. data/lib/aidp/cli/checkpoint_command.rb +198 -0
  5. data/lib/aidp/cli/config_command.rb +71 -0
  6. data/lib/aidp/cli/enhanced_input.rb +2 -0
  7. data/lib/aidp/cli/first_run_wizard.rb +8 -7
  8. data/lib/aidp/cli/harness_command.rb +102 -0
  9. data/lib/aidp/cli/jobs_command.rb +3 -3
  10. data/lib/aidp/cli/mcp_dashboard.rb +4 -3
  11. data/lib/aidp/cli/models_command.rb +662 -0
  12. data/lib/aidp/cli/providers_command.rb +223 -0
  13. data/lib/aidp/cli.rb +35 -456
  14. data/lib/aidp/daemon/runner.rb +2 -2
  15. data/lib/aidp/debug_mixin.rb +2 -9
  16. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  17. data/lib/aidp/execute/checkpoint_display.rb +38 -37
  18. data/lib/aidp/execute/interactive_repl.rb +2 -1
  19. data/lib/aidp/execute/prompt_manager.rb +4 -4
  20. data/lib/aidp/execute/work_loop_runner.rb +253 -56
  21. data/lib/aidp/execute/workflow_selector.rb +2 -2
  22. data/lib/aidp/harness/config_loader.rb +20 -11
  23. data/lib/aidp/harness/config_manager.rb +5 -5
  24. data/lib/aidp/harness/config_schema.rb +30 -8
  25. data/lib/aidp/harness/configuration.rb +105 -4
  26. data/lib/aidp/harness/enhanced_runner.rb +24 -15
  27. data/lib/aidp/harness/error_handler.rb +26 -5
  28. data/lib/aidp/harness/filter_strategy.rb +45 -0
  29. data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
  30. data/lib/aidp/harness/model_cache.rb +269 -0
  31. data/lib/aidp/harness/model_discovery_service.rb +259 -0
  32. data/lib/aidp/harness/model_registry.rb +201 -0
  33. data/lib/aidp/harness/output_filter.rb +136 -0
  34. data/lib/aidp/harness/provider_manager.rb +18 -3
  35. data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
  36. data/lib/aidp/harness/runner.rb +5 -0
  37. data/lib/aidp/harness/test_runner.rb +165 -27
  38. data/lib/aidp/harness/thinking_depth_manager.rb +223 -7
  39. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
  40. data/lib/aidp/logger.rb +35 -5
  41. data/lib/aidp/providers/adapter.rb +2 -4
  42. data/lib/aidp/providers/anthropic.rb +141 -128
  43. data/lib/aidp/providers/base.rb +98 -2
  44. data/lib/aidp/providers/capability_registry.rb +0 -1
  45. data/lib/aidp/providers/codex.rb +49 -67
  46. data/lib/aidp/providers/cursor.rb +71 -59
  47. data/lib/aidp/providers/gemini.rb +44 -60
  48. data/lib/aidp/providers/github_copilot.rb +2 -66
  49. data/lib/aidp/providers/kilocode.rb +24 -80
  50. data/lib/aidp/providers/opencode.rb +24 -80
  51. data/lib/aidp/safe_directory.rb +10 -3
  52. data/lib/aidp/setup/wizard.rb +345 -8
  53. data/lib/aidp/storage/csv_storage.rb +9 -3
  54. data/lib/aidp/storage/file_manager.rb +8 -2
  55. data/lib/aidp/storage/json_storage.rb +9 -3
  56. data/lib/aidp/version.rb +1 -1
  57. data/lib/aidp/watch/build_processor.rb +40 -1
  58. data/lib/aidp/watch/change_request_processor.rb +659 -0
  59. data/lib/aidp/watch/plan_generator.rb +93 -14
  60. data/lib/aidp/watch/plan_processor.rb +71 -8
  61. data/lib/aidp/watch/repository_client.rb +85 -20
  62. data/lib/aidp/watch/review_processor.rb +3 -3
  63. data/lib/aidp/watch/runner.rb +37 -0
  64. data/lib/aidp/watch/state_store.rb +46 -1
  65. data/lib/aidp/workflows/guided_agent.rb +3 -3
  66. data/lib/aidp/workstream_executor.rb +5 -2
  67. data/lib/aidp.rb +4 -0
  68. data/templates/aidp-development.yml.example +2 -2
  69. data/templates/aidp-production.yml.example +3 -3
  70. data/templates/aidp.yml.example +53 -0
  71. metadata +14 -1
@@ -29,32 +29,103 @@ module Aidp
29
29
  def initialize(provider_name: nil, verbose: false)
30
30
  @provider_name = provider_name
31
31
  @verbose = verbose
32
+ @providers_attempted = []
32
33
  end
33
34
 
34
35
  def generate(issue)
35
- provider = resolve_provider
36
- if provider
37
- generate_with_provider(provider, issue)
38
- else
39
- display_message("⚠️ No active provider available. Falling back to heuristic plan.", type: :warn)
40
- heuristic_plan(issue)
36
+ Aidp.log_debug("plan_generator", "generate.start", provider: @provider_name, issue: issue[:number])
37
+
38
+ # Try providers in fallback chain order
39
+ providers_to_try = build_provider_fallback_chain
40
+ Aidp.log_debug("plan_generator", "fallback_chain", providers: providers_to_try, count: providers_to_try.size)
41
+
42
+ providers_to_try.each do |provider_name|
43
+ next if @providers_attempted.include?(provider_name)
44
+
45
+ Aidp.log_debug("plan_generator", "trying_provider", provider: provider_name, attempted: @providers_attempted)
46
+
47
+ provider = resolve_provider(provider_name)
48
+ unless provider
49
+ Aidp.log_debug("plan_generator", "provider_unavailable", provider: provider_name, reason: "not resolved")
50
+ @providers_attempted << provider_name
51
+ next
52
+ end
53
+
54
+ begin
55
+ Aidp.log_info("plan_generator", "generate_with_provider", provider: provider_name, issue: issue[:number])
56
+ result = generate_with_provider(provider, issue, provider_name)
57
+ if result
58
+ Aidp.log_info("plan_generator", "generation_success", provider: provider_name, issue: issue[:number])
59
+ return result
60
+ end
61
+
62
+ # Provider returned nil - try next provider
63
+ Aidp.log_warn("plan_generator", "provider_returned_nil", provider: provider_name)
64
+ @providers_attempted << provider_name
65
+ rescue => e
66
+ # Log error and try next provider in chain
67
+ Aidp.log_warn("plan_generator", "provider_failed", provider: provider_name, error: e.message, error_class: e.class.name)
68
+ @providers_attempted << provider_name
69
+ end
41
70
  end
71
+
72
+ # All providers exhausted, fall back to heuristic
73
+ Aidp.log_warn("plan_generator", "all_providers_exhausted", attempted: @providers_attempted, falling_back: "heuristic")
74
+ display_message("⚠️ All providers unavailable or failed. Falling back to heuristic plan.", type: :warn)
75
+ heuristic_plan(issue)
42
76
  rescue => e
43
- display_message("⚠️ Plan generation failed (#{e.message}). Using heuristic.", type: :warn)
77
+ Aidp.log_error("plan_generator", "generation_failed_unexpectedly", error: e.message, backtrace: e.backtrace&.first(3))
78
+ display_message("⚠️ Plan generation failed unexpectedly (#{e.message}). Using heuristic.", type: :warn)
44
79
  heuristic_plan(issue)
45
80
  end
46
81
 
47
82
  private
48
83
 
49
- def resolve_provider
50
- provider_name = @provider_name || detect_default_provider
84
+ def build_provider_fallback_chain
85
+ # Start with specified provider or default
86
+ primary_provider = @provider_name || detect_default_provider
87
+ providers = []
88
+
89
+ # Add primary provider first
90
+ providers << primary_provider if primary_provider
91
+
92
+ # Try to get fallback chain from config
93
+ begin
94
+ config_manager = Aidp::Harness::ConfigManager.new(Dir.pwd)
95
+ fallback_providers = config_manager.fallback_providers || []
96
+
97
+ # Add fallback providers that aren't already in the list
98
+ fallback_providers.each do |fallback|
99
+ providers << fallback unless providers.include?(fallback)
100
+ end
101
+ rescue => e
102
+ Aidp.log_debug("plan_generator", "config_fallback_unavailable", error: e.message)
103
+ end
104
+
105
+ # If we still have no providers, add cursor as last resort
106
+ providers << "cursor" if providers.empty?
107
+
108
+ # Remove duplicates while preserving order
109
+ providers.uniq
110
+ end
111
+
112
+ def resolve_provider(provider_name = nil)
113
+ provider_name ||= @provider_name || detect_default_provider
51
114
  return nil unless provider_name
52
115
 
116
+ Aidp.log_debug("plan_generator", "resolve_provider", provider: provider_name)
117
+
53
118
  provider = Aidp::ProviderManager.get_provider(provider_name, use_harness: false)
54
- return provider if provider&.available?
55
119
 
120
+ if provider&.available?
121
+ Aidp.log_debug("plan_generator", "provider_resolved", provider: provider_name, available: true)
122
+ return provider
123
+ end
124
+
125
+ Aidp.log_debug("plan_generator", "provider_not_available", provider: provider_name, available: provider&.available?)
56
126
  nil
57
127
  rescue => e
128
+ Aidp.log_warn("plan_generator", "resolve_provider_failed", provider: provider_name, error: e.message)
58
129
  display_message("⚠️ Failed to resolve provider #{provider_name}: #{e.message}", type: :warn)
59
130
  nil
60
131
  end
@@ -66,9 +137,11 @@ module Aidp
66
137
  "cursor"
67
138
  end
68
139
 
69
- def generate_with_provider(provider, issue)
140
+ def generate_with_provider(provider, issue, provider_name = "unknown")
70
141
  payload = build_prompt(issue)
71
142
 
143
+ Aidp.log_debug("plan_generator", "sending_to_provider", provider: provider_name, prompt_length: payload.length)
144
+
72
145
  if @verbose
73
146
  display_message("\n--- Plan Generation Prompt ---", type: :muted)
74
147
  display_message(payload.strip, type: :muted)
@@ -77,6 +150,8 @@ module Aidp
77
150
 
78
151
  response = provider.send_message(prompt: payload)
79
152
 
153
+ Aidp.log_debug("plan_generator", "provider_response_received", provider: provider_name, response_length: response&.length || 0)
154
+
80
155
  if @verbose
81
156
  display_message("\n--- Provider Response ---", type: :muted)
82
157
  display_message(response.strip, type: :muted)
@@ -85,10 +160,14 @@ module Aidp
85
160
 
86
161
  parsed = parse_structured_response(response)
87
162
 
88
- return parsed if parsed
163
+ if parsed
164
+ Aidp.log_debug("plan_generator", "response_parsed", provider: provider_name, has_summary: !parsed[:summary].to_s.empty?, tasks_count: parsed[:tasks]&.size || 0)
165
+ return parsed
166
+ end
89
167
 
90
- display_message("⚠️ Unable to parse provider response. Using heuristic plan.", type: :warn)
91
- heuristic_plan(issue)
168
+ Aidp.log_warn("plan_generator", "parse_failed", provider: provider_name)
169
+ display_message("⚠️ Unable to parse #{provider_name} response. Trying next provider.", type: :warn)
170
+ nil
92
171
  end
93
172
 
94
173
  def build_prompt(issue)
@@ -41,21 +41,46 @@ module Aidp
41
41
 
42
42
  def process(issue)
43
43
  number = issue[:number]
44
- if @state_store.plan_processed?(number)
45
- display_message("ℹ️ Plan for issue ##{number} already posted. Skipping.", type: :muted)
46
- return
44
+ existing_plan = @state_store.plan_data(number)
45
+
46
+ if existing_plan
47
+ display_message("🔄 Re-planning for issue ##{number} (iteration #{@state_store.plan_iteration_count(number) + 1})", type: :info)
48
+ else
49
+ display_message("🧠 Generating plan for issue ##{number} (#{issue[:title]})", type: :info)
47
50
  end
48
51
 
49
- display_message("🧠 Generating plan for issue ##{number} (#{issue[:title]})", type: :info)
50
52
  plan_data = @plan_generator.generate(issue)
51
53
 
52
54
  # Fetch the user who added the most recent label
53
55
  label_actor = @repository_client.most_recent_label_actor(number)
54
56
 
55
- comment_body = build_comment(issue: issue, plan: plan_data, label_actor: label_actor)
56
- @repository_client.post_comment(number, comment_body)
57
+ # If updating existing plan, archive the previous content
58
+ archived_content = existing_plan ? archive_previous_plan(number, existing_plan) : nil
59
+
60
+ comment_body = build_comment(issue: issue, plan: plan_data, label_actor: label_actor, archived_content: archived_content)
61
+
62
+ if existing_plan && existing_plan["comment_id"]
63
+ # Update existing comment
64
+ @repository_client.update_comment(existing_plan["comment_id"], comment_body)
65
+ display_message("📝 Updated plan comment for issue ##{number}", type: :success)
66
+ elsif existing_plan
67
+ # Try to find existing comment by header
68
+ existing_comment = @repository_client.find_comment(number, COMMENT_HEADER)
69
+ if existing_comment
70
+ @repository_client.update_comment(existing_comment[:id], comment_body)
71
+ display_message("📝 Updated plan comment for issue ##{number}", type: :success)
72
+ plan_data = plan_data.merge(comment_id: existing_comment[:id])
73
+ else
74
+ # Fallback to posting new comment if we can't find the old one
75
+ @repository_client.post_comment(number, comment_body)
76
+ display_message("💬 Posted new plan comment for issue ##{number}", type: :success)
77
+ end
78
+ else
79
+ # First time planning - post new comment
80
+ @repository_client.post_comment(number, comment_body)
81
+ display_message("💬 Posted plan comment for issue ##{number}", type: :success)
82
+ end
57
83
 
58
- display_message("💬 Posted plan comment for issue ##{number}", type: :success)
59
84
  @state_store.record_plan(number, plan_data.merge(comment_body: comment_body, comment_hint: COMMENT_HEADER))
60
85
 
61
86
  # Update labels: remove plan trigger, add appropriate status label
@@ -64,6 +89,31 @@ module Aidp
64
89
 
65
90
  private
66
91
 
92
+ def archive_previous_plan(number, existing_plan)
93
+ iteration = @state_store.plan_iteration_count(number)
94
+ timestamp = existing_plan["posted_at"] || "unknown"
95
+
96
+ archived_parts = []
97
+ archived_parts << "<!-- ARCHIVED_PLAN_START iteration=#{iteration} timestamp=#{timestamp} -->"
98
+ archived_parts << "<details>"
99
+ archived_parts << "<summary>📋 Previous Plan (Iteration #{iteration}) - #{timestamp}</summary>"
100
+ archived_parts << ""
101
+ archived_parts << "<!-- ARCHIVED_PLAN_SUMMARY_START -->"
102
+ archived_parts << "### Plan Summary"
103
+ archived_parts << existing_plan["summary"].to_s
104
+ archived_parts << "<!-- ARCHIVED_PLAN_SUMMARY_END -->"
105
+ archived_parts << ""
106
+ archived_parts << "<!-- ARCHIVED_PLAN_TASKS_START -->"
107
+ archived_parts << "### Proposed Tasks"
108
+ archived_parts << format_bullets(Array(existing_plan["tasks"]), placeholder: "_No tasks_")
109
+ archived_parts << "<!-- ARCHIVED_PLAN_TASKS_END -->"
110
+ archived_parts << ""
111
+ archived_parts << "</details>"
112
+ archived_parts << "<!-- ARCHIVED_PLAN_END -->"
113
+
114
+ archived_parts.join("\n")
115
+ end
116
+
67
117
  def update_labels_after_plan(number, plan_data)
68
118
  questions = Array(plan_data[:questions])
69
119
  has_questions = questions.any? && !questions.all? { |q| q.to_s.strip.empty? }
@@ -85,7 +135,7 @@ module Aidp
85
135
  end
86
136
  end
87
137
 
88
- def build_comment(issue:, plan:, label_actor: nil)
138
+ def build_comment(issue:, plan:, label_actor: nil, archived_content: nil)
89
139
  summary = plan[:summary].to_s.strip
90
140
  tasks = Array(plan[:tasks])
91
141
  questions = Array(plan[:questions])
@@ -104,14 +154,27 @@ module Aidp
104
154
  parts << "**Issue**: [##{issue[:number]}](#{issue[:url]})"
105
155
  parts << "**Title**: #{issue[:title]}"
106
156
  parts << ""
157
+
158
+ # Add archived content if this is a plan update
159
+ if archived_content
160
+ parts << archived_content
161
+ parts << ""
162
+ end
163
+
164
+ parts << "<!-- PLAN_SUMMARY_START -->"
107
165
  parts << "### Plan Summary"
108
166
  parts << (summary.empty? ? "_No summary generated_" : summary)
167
+ parts << "<!-- PLAN_SUMMARY_END -->"
109
168
  parts << ""
169
+ parts << "<!-- PLAN_TASKS_START -->"
110
170
  parts << "### Proposed Tasks"
111
171
  parts << format_bullets(tasks, placeholder: "_Pending task breakdown_")
172
+ parts << "<!-- PLAN_TASKS_END -->"
112
173
  parts << ""
174
+ parts << "<!-- CLARIFYING_QUESTIONS_START -->"
113
175
  parts << "### Clarifying Questions"
114
176
  parts << format_numbered(questions, placeholder: "_No questions identified_")
177
+ parts << "<!-- CLARIFYING_QUESTIONS_END -->"
115
178
  parts << ""
116
179
 
117
180
  # Add instructions based on whether there are questions
@@ -61,6 +61,14 @@ module Aidp
61
61
  gh_available? ? post_comment_via_gh(number, body) : post_comment_via_api(number, body)
62
62
  end
63
63
 
64
+ def find_comment(number, header_text)
65
+ gh_available? ? find_comment_via_gh(number, header_text) : find_comment_via_api(number, header_text)
66
+ end
67
+
68
+ def update_comment(comment_id, body)
69
+ gh_available? ? update_comment_via_gh(comment_id, body) : update_comment_via_api(comment_id, body)
70
+ end
71
+
64
72
  def create_pull_request(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil)
65
73
  gh_available? ? create_pull_request_via_gh(title: title, body: body, head: head, base: base, issue_number: issue_number, draft: draft, assignee: assignee) : raise("GitHub CLI not available - cannot create PR")
66
74
  end
@@ -108,6 +116,10 @@ module Aidp
108
116
  gh_available? ? list_pull_requests_via_gh(labels: labels, state: state) : list_pull_requests_via_api(labels: labels, state: state)
109
117
  end
110
118
 
119
+ def fetch_pr_comments(number)
120
+ gh_available? ? fetch_pr_comments_via_gh(number) : fetch_pr_comments_via_api(number)
121
+ end
122
+
111
123
  private
112
124
 
113
125
  def list_issues_via_gh(labels:, state:)
@@ -162,27 +174,11 @@ module Aidp
162
174
  raise "GitHub API error (#{response.code})" unless response.code == "200"
163
175
 
164
176
  data = JSON.parse(response.body)
165
- comments = fetch_comments_via_api(number)
177
+ comments = fetch_pr_comments_via_api(number)
166
178
  data["comments"] = comments
167
179
  normalize_issue_detail_api(data)
168
180
  end
169
181
 
170
- def fetch_comments_via_api(number)
171
- uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/comments")
172
- response = Net::HTTP.get_response(uri)
173
- return [] unless response.code == "200"
174
-
175
- JSON.parse(response.body).map do |raw|
176
- {
177
- "body" => raw["body"],
178
- "author" => raw.dig("user", "login"),
179
- "createdAt" => raw["created_at"]
180
- }
181
- end
182
- rescue
183
- []
184
- end
185
-
186
182
  def post_comment_via_gh(number, body)
187
183
  cmd = ["gh", "issue", "comment", number.to_s, "--repo", full_repo, "--body", body]
188
184
  stdout, stderr, status = Open3.capture3(*cmd)
@@ -205,6 +201,44 @@ module Aidp
205
201
  response.body
206
202
  end
207
203
 
204
+ def find_comment_via_gh(number, header_text)
205
+ comments = fetch_pr_comments_via_gh(number)
206
+ comments.find { |comment| comment[:body]&.include?(header_text) }
207
+ rescue => e
208
+ Aidp.log_warn("repository_client", "Failed to find comment", error: e.message)
209
+ nil
210
+ end
211
+
212
+ def find_comment_via_api(number, header_text)
213
+ comments = fetch_pr_comments_via_api(number)
214
+ comments.find { |comment| comment[:body]&.include?(header_text) }
215
+ rescue => e
216
+ Aidp.log_warn("repository_client", "Failed to find comment", error: e.message)
217
+ nil
218
+ end
219
+
220
+ def update_comment_via_gh(comment_id, body)
221
+ cmd = ["gh", "api", "repos/#{full_repo}/issues/comments/#{comment_id}", "-X", "PATCH", "-f", "body=#{body}"]
222
+ stdout, stderr, status = Open3.capture3(*cmd)
223
+ raise "Failed to update comment via gh: #{stderr.strip}" unless status.success?
224
+
225
+ stdout.strip
226
+ end
227
+
228
+ def update_comment_via_api(comment_id, body)
229
+ uri = URI("https://api.github.com/repos/#{full_repo}/issues/comments/#{comment_id}")
230
+ request = Net::HTTP::Patch.new(uri)
231
+ request["Content-Type"] = "application/json"
232
+ request.body = JSON.dump({body: body})
233
+
234
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
235
+ http.request(request)
236
+ end
237
+
238
+ raise "GitHub API update comment failed (#{response.code})" unless response.code.start_with?("2")
239
+ response.body
240
+ end
241
+
208
242
  def create_pull_request_via_gh(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil)
209
243
  cmd = [
210
244
  "gh", "pr", "create",
@@ -535,6 +569,27 @@ module Aidp
535
569
  response.body
536
570
  end
537
571
 
572
+ def fetch_pr_comments_via_gh(number)
573
+ cmd = ["gh", "api", "repos/#{full_repo}/issues/#{number}/comments", "--jq", "."]
574
+ stdout, stderr, status = Open3.capture3(*cmd)
575
+ raise "Failed to fetch PR comments via gh: #{stderr.strip}" unless status.success?
576
+
577
+ JSON.parse(stdout).map { |raw| normalize_pr_comment(raw) }
578
+ rescue JSON::ParserError => e
579
+ raise "Failed to parse PR comments response: #{e.message}"
580
+ end
581
+
582
+ def fetch_pr_comments_via_api(number)
583
+ uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/comments")
584
+ response = Net::HTTP.get_response(uri)
585
+ return [] unless response.code == "200"
586
+
587
+ JSON.parse(response.body).map { |raw| normalize_pr_comment(raw) }
588
+ rescue => e
589
+ Aidp.log_warn("repository_client", "Failed to fetch PR comments", error: e.message)
590
+ []
591
+ end
592
+
538
593
  # Normalization methods for PRs
539
594
  def normalize_pull_request(raw)
540
595
  {
@@ -691,14 +746,24 @@ module Aidp
691
746
  def normalize_comment(comment)
692
747
  if comment.is_a?(Hash)
693
748
  {
694
- "body" => comment["body"],
695
- "author" => comment["author"] || comment.dig("user", "login"),
696
- "createdAt" => comment["createdAt"] || comment["created_at"]
749
+ "body" => comment["body"] || comment[:body],
750
+ "author" => comment["author"] || comment[:author] || comment.dig("user", "login"),
751
+ "createdAt" => comment["createdAt"] || comment[:created_at] || comment["created_at"]
697
752
  }
698
753
  else
699
754
  {"body" => comment.to_s}
700
755
  end
701
756
  end
757
+
758
+ def normalize_pr_comment(raw)
759
+ {
760
+ id: raw["id"],
761
+ body: raw["body"],
762
+ author: raw.dig("user", "login"),
763
+ created_at: raw["created_at"],
764
+ updated_at: raw["updated_at"]
765
+ }
766
+ end
702
767
  end
703
768
  end
704
769
  end
@@ -23,7 +23,7 @@ module Aidp
23
23
 
24
24
  attr_reader :review_label
25
25
 
26
- def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, verbose: false)
26
+ def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, verbose: false, reviewers: nil)
27
27
  @repository_client = repository_client
28
28
  @state_store = state_store
29
29
  @provider_name = provider_name
@@ -33,8 +33,8 @@ module Aidp
33
33
  # Load label configuration
34
34
  @review_label = label_config[:review_trigger] || label_config["review_trigger"] || DEFAULT_REVIEW_LABEL
35
35
 
36
- # Initialize reviewers
37
- @reviewers = [
36
+ # Initialize reviewers (allow dependency injection for testing)
37
+ @reviewers = reviewers || [
38
38
  Reviewers::SeniorDevReviewer.new(provider_name: provider_name),
39
39
  Reviewers::SecurityReviewer.new(provider_name: provider_name),
40
40
  Reviewers::PerformanceReviewer.new(provider_name: provider_name)
@@ -12,6 +12,7 @@ require_relative "build_processor"
12
12
  require_relative "../auto_update"
13
13
  require_relative "review_processor"
14
14
  require_relative "ci_fix_processor"
15
+ require_relative "change_request_processor"
15
16
 
16
17
  module Aidp
17
18
  module Watch
@@ -77,6 +78,16 @@ module Aidp
77
78
  label_config: label_config,
78
79
  verbose: verbose
79
80
  )
81
+ @change_request_processor = ChangeRequestProcessor.new(
82
+ repository_client: @repository_client,
83
+ state_store: @state_store,
84
+ provider_name: provider_name,
85
+ project_dir: project_dir,
86
+ label_config: label_config,
87
+ change_request_config: safety_config[:pr_change_requests] || safety_config["pr_change_requests"] || {},
88
+ safety_config: safety_config[:safety] || safety_config["safety"] || {},
89
+ verbose: verbose
90
+ )
80
91
  end
81
92
 
82
93
  def start
@@ -122,6 +133,7 @@ module Aidp
122
133
  check_for_updates_if_due
123
134
  process_review_triggers
124
135
  process_ci_fix_triggers
136
+ process_change_request_triggers
125
137
  end
126
138
 
127
139
  def process_plan_triggers
@@ -230,6 +242,31 @@ module Aidp
230
242
  end
231
243
  end
232
244
 
245
+ def process_change_request_triggers
246
+ change_request_label = @change_request_processor.change_request_label
247
+ prs = @repository_client.list_pull_requests(labels: [change_request_label], state: "open")
248
+ Aidp.log_debug("watch_runner", "change_request_poll", label: change_request_label, total: prs.size)
249
+ prs.each do |pr|
250
+ unless pr_has_label?(pr, change_request_label)
251
+ Aidp.log_debug("watch_runner", "change_request_skip_label_mismatch", pr: pr[:number], labels: pr[:labels])
252
+ next
253
+ end
254
+
255
+ detailed = @repository_client.fetch_pull_request(pr[:number])
256
+
257
+ # Check author authorization before processing
258
+ unless @safety_checker.should_process_issue?(detailed, enforce: false)
259
+ Aidp.log_debug("watch_runner", "change_request_skip_unauthorized_author", pr: detailed[:number], author: detailed[:author])
260
+ next
261
+ end
262
+
263
+ Aidp.log_debug("watch_runner", "change_request_process", pr: detailed[:number])
264
+ @change_request_processor.process(detailed)
265
+ rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
266
+ Aidp.log_warn("watch_runner", "unauthorized PR author", pr: pr[:number], error: e.message)
267
+ end
268
+ end
269
+
233
270
  def issue_has_label?(issue, label)
234
271
  Array(issue[:labels]).any? do |issue_label|
235
272
  name = issue_label.is_a?(Hash) ? issue_label["name"] : issue_label.to_s
@@ -27,14 +27,26 @@ module Aidp
27
27
  plans[issue_number.to_s]
28
28
  end
29
29
 
30
+ def plan_iteration_count(issue_number)
31
+ plan = plans[issue_number.to_s]
32
+ return 0 unless plan
33
+ plan["iteration"] || 1
34
+ end
35
+
30
36
  def record_plan(issue_number, data)
37
+ existing_plan = plans[issue_number.to_s]
38
+ iteration = existing_plan ? (existing_plan["iteration"] || 1) + 1 : 1
39
+
31
40
  payload = {
32
41
  "summary" => data[:summary],
33
42
  "tasks" => data[:tasks],
34
43
  "questions" => data[:questions],
35
44
  "comment_body" => data[:comment_body],
36
45
  "comment_hint" => data[:comment_hint],
37
- "posted_at" => data[:posted_at] || Time.now.utc.iso8601
46
+ "comment_id" => data[:comment_id],
47
+ "posted_at" => data[:posted_at] || Time.now.utc.iso8601,
48
+ "iteration" => iteration,
49
+ "previous_iteration_at" => existing_plan ? existing_plan["posted_at"] : nil
38
50
  }.compact
39
51
 
40
52
  plans[issue_number.to_s] = payload
@@ -96,6 +108,34 @@ module Aidp
96
108
  save!
97
109
  end
98
110
 
111
+ # Change request tracking methods
112
+ def change_request_processed?(pr_number)
113
+ change_requests.key?(pr_number.to_s)
114
+ end
115
+
116
+ def change_request_data(pr_number)
117
+ change_requests[pr_number.to_s]
118
+ end
119
+
120
+ def record_change_request(pr_number, data)
121
+ payload = {
122
+ "status" => data[:status],
123
+ "timestamp" => data[:timestamp] || Time.now.utc.iso8601,
124
+ "changes_applied" => data[:changes_applied],
125
+ "commits" => data[:commits],
126
+ "reason" => data[:reason],
127
+ "clarification_count" => data[:clarification_count]
128
+ }.compact
129
+
130
+ change_requests[pr_number.to_s] = payload
131
+ save!
132
+ end
133
+
134
+ def reset_change_request_state(pr_number)
135
+ change_requests.delete(pr_number.to_s)
136
+ save!
137
+ end
138
+
99
139
  private
100
140
 
101
141
  def ensure_directory
@@ -126,6 +166,7 @@ module Aidp
126
166
  base["builds"] ||= {}
127
167
  base["reviews"] ||= {}
128
168
  base["ci_fixes"] ||= {}
169
+ base["change_requests"] ||= {}
129
170
  base
130
171
  end
131
172
  end
@@ -146,6 +187,10 @@ module Aidp
146
187
  state["ci_fixes"]
147
188
  end
148
189
 
190
+ def change_requests
191
+ state["change_requests"]
192
+ end
193
+
149
194
  def stringify_keys(hash)
150
195
  return {} unless hash
151
196
 
@@ -19,7 +19,7 @@ module Aidp
19
19
 
20
20
  class ConversationError < StandardError; end
21
21
 
22
- def initialize(project_dir, prompt: nil, use_enhanced_input: true, verbose: false)
22
+ def initialize(project_dir, prompt: nil, use_enhanced_input: true, verbose: false, config_manager: nil, provider_manager: nil)
23
23
  @project_dir = project_dir
24
24
 
25
25
  # Use EnhancedInput with Reline for full readline-style key bindings
@@ -29,8 +29,8 @@ module Aidp
29
29
  prompt || TTY::Prompt.new
30
30
  end
31
31
 
32
- @config_manager = Aidp::Harness::ConfigManager.new(project_dir)
33
- @provider_manager = Aidp::Harness::ProviderManager.new(@config_manager, prompt: @prompt)
32
+ @config_manager = config_manager || Aidp::Harness::ConfigManager.new(project_dir)
33
+ @provider_manager = provider_manager || Aidp::Harness::ProviderManager.new(@config_manager, prompt: @prompt)
34
34
  @conversation_history = []
35
35
  @user_input = {}
36
36
  @verbose = verbose
@@ -156,8 +156,11 @@ module Aidp
156
156
  )
157
157
 
158
158
  # Log error and exit
159
- warn("Error in workstream #{slug}: #{e.message}")
160
- warn(e.backtrace.first(5).join("\n"))
159
+ # Suppress backtrace noise during tests while keeping it for production debugging
160
+ unless ENV["RSPEC_RUNNING"] == "true"
161
+ warn("Error in workstream #{slug}: #{e.message}")
162
+ warn(e.backtrace.first(5).join("\n"))
163
+ end
161
164
  exit(1)
162
165
  end
163
166
 
data/lib/aidp.rb CHANGED
@@ -93,6 +93,10 @@ require_relative "aidp/harness/state_manager"
93
93
  require_relative "aidp/harness/error_handler"
94
94
  require_relative "aidp/harness/status_display"
95
95
  require_relative "aidp/harness/runner"
96
+ require_relative "aidp/harness/filter_strategy"
97
+ require_relative "aidp/harness/generic_filter_strategy"
98
+ require_relative "aidp/harness/rspec_filter_strategy"
99
+ require_relative "aidp/harness/output_filter"
96
100
 
97
101
  # UI components
98
102
  require_relative "aidp/harness/ui/spinner_helper"
@@ -137,7 +137,7 @@ providers:
137
137
  file_upload: true
138
138
  code_generation: true
139
139
  analysis: true
140
- streaming: true
140
+ supports_json_mode: true
141
141
 
142
142
  # Monitoring configuration (enhanced for development)
143
143
  monitoring:
@@ -243,7 +243,7 @@ providers:
243
243
  code_generation: true
244
244
  analysis: true
245
245
  vision: true
246
- streaming: true
246
+ supports_json_mode: true
247
247
  function_calling: true
248
248
  tool_use: true
249
249