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,448 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "fileutils"
5
+ require "json"
6
+ require "time"
7
+
8
+ require_relative "../message_display"
9
+ require_relative "../provider_manager"
10
+ require_relative "../harness/config_manager"
11
+ require_relative "../execute/prompt_manager"
12
+ require_relative "../harness/runner"
13
+ require_relative "../harness/state_manager"
14
+
15
+ module Aidp
16
+ module Watch
17
+ # Handles the aidp-fix-ci label trigger by analyzing CI failures
18
+ # and automatically fixing them with commits pushed to the PR branch.
19
+ class CiFixProcessor
20
+ include Aidp::MessageDisplay
21
+
22
+ # Default label names
23
+ DEFAULT_CI_FIX_LABEL = "aidp-fix-ci"
24
+
25
+ COMMENT_HEADER = "## 🤖 AIDP CI Fix"
26
+ MAX_FIX_ATTEMPTS = 3
27
+
28
+ attr_reader :ci_fix_label
29
+
30
+ def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, verbose: false)
31
+ @repository_client = repository_client
32
+ @state_store = state_store
33
+ @provider_name = provider_name
34
+ @project_dir = project_dir
35
+ @verbose = verbose
36
+
37
+ # Load label configuration
38
+ @ci_fix_label = label_config[:ci_fix_trigger] || label_config["ci_fix_trigger"] || DEFAULT_CI_FIX_LABEL
39
+ end
40
+
41
+ def process(pr)
42
+ number = pr[:number]
43
+
44
+ # Check if already processed successfully
45
+ if @state_store.ci_fix_completed?(number)
46
+ display_message("â„šī¸ CI fix for PR ##{number} already completed. Skipping.", type: :muted)
47
+ return
48
+ end
49
+
50
+ display_message("🔧 Analyzing CI failures for PR ##{number} (#{pr[:title]})", type: :info)
51
+
52
+ # Fetch PR details
53
+ pr_data = @repository_client.fetch_pull_request(number)
54
+ ci_status = @repository_client.fetch_ci_status(number)
55
+
56
+ # Check if there are failures
57
+ if ci_status[:state] == "success"
58
+ display_message("✅ CI is passing for PR ##{number}. No fixes needed.", type: :success)
59
+ post_success_comment(pr_data)
60
+ @state_store.record_ci_fix(number, {status: "no_failures", timestamp: Time.now.utc.iso8601})
61
+ begin
62
+ @repository_client.remove_labels(number, @ci_fix_label)
63
+ rescue
64
+ nil
65
+ end
66
+ return
67
+ end
68
+
69
+ if ci_status[:state] == "pending"
70
+ display_message("âŗ CI is still running for PR ##{number}. Skipping for now.", type: :muted)
71
+ return
72
+ end
73
+
74
+ # Get failed checks
75
+ failed_checks = ci_status[:checks].select { |check| check[:conclusion] == "failure" }
76
+
77
+ if failed_checks.empty?
78
+ display_message("âš ī¸ No specific failed checks found for PR ##{number}.", type: :warn)
79
+ return
80
+ end
81
+
82
+ display_message("Found #{failed_checks.length} failed check(s):", type: :info)
83
+ failed_checks.each do |check|
84
+ display_message(" - #{check[:name]}", type: :muted)
85
+ end
86
+
87
+ # Analyze failures and generate fixes
88
+ fix_result = analyze_and_fix(pr_data: pr_data, ci_status: ci_status, failed_checks: failed_checks)
89
+
90
+ # Log the fix attempt
91
+ log_ci_fix(number, fix_result)
92
+
93
+ if fix_result[:success]
94
+ handle_success(pr: pr_data, fix_result: fix_result)
95
+ else
96
+ handle_failure(pr: pr_data, fix_result: fix_result)
97
+ end
98
+ rescue => e
99
+ display_message("❌ CI fix failed: #{e.message}", type: :error)
100
+ Aidp.log_error("ci_fix_processor", "CI fix failed", pr: pr[:number], error: e.message, backtrace: e.backtrace&.first(10))
101
+
102
+ # Post error comment
103
+ error_comment = <<~COMMENT
104
+ #{COMMENT_HEADER}
105
+
106
+ ❌ Automated CI fix failed: #{e.message}
107
+
108
+ Please investigate the CI failures manually or retry by re-adding the `#{@ci_fix_label}` label.
109
+ COMMENT
110
+ begin
111
+ @repository_client.post_comment(pr[:number], error_comment)
112
+ rescue
113
+ nil
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def analyze_and_fix(pr_data:, ci_status:, failed_checks:)
120
+ # Fetch logs for failed checks (if available)
121
+ failure_details = failed_checks.map do |check|
122
+ {
123
+ name: check[:name],
124
+ conclusion: check[:conclusion],
125
+ output: check[:output],
126
+ details_url: check[:details_url]
127
+ }
128
+ end
129
+
130
+ # Use AI to analyze failures and propose fixes
131
+ analysis = analyze_failures_with_ai(pr_data: pr_data, failures: failure_details)
132
+
133
+ if analysis[:can_fix]
134
+ # Checkout the PR branch and apply fixes
135
+ checkout_pr_branch(pr_data)
136
+
137
+ # Apply the proposed fixes
138
+ apply_fixes(analysis[:fixes])
139
+
140
+ # Commit and push
141
+ if commit_and_push(pr_data, analysis)
142
+ {success: true, analysis: analysis, commit_created: true}
143
+ else
144
+ {success: false, analysis: analysis, reason: "No changes to commit"}
145
+ end
146
+ else
147
+ {success: false, analysis: analysis, reason: analysis[:reason] || "Cannot automatically fix"}
148
+ end
149
+ rescue => e
150
+ {success: false, error: e.message, backtrace: e.backtrace&.first(5)}
151
+ end
152
+
153
+ def analyze_failures_with_ai(pr_data:, failures:)
154
+ provider_name = @provider_name || detect_default_provider
155
+ provider = Aidp::ProviderManager.get_provider(provider_name, use_harness: false)
156
+
157
+ user_prompt = build_ci_analysis_prompt(pr_data: pr_data, failures: failures)
158
+ full_prompt = "#{ci_fix_system_prompt}\n\n#{user_prompt}"
159
+
160
+ response = provider.send_message(prompt: full_prompt)
161
+ content = response.to_s.strip
162
+
163
+ # Extract JSON from response (handle code fences)
164
+ json_content = extract_json(content)
165
+
166
+ # Parse JSON response
167
+ parsed = JSON.parse(json_content)
168
+
169
+ {
170
+ can_fix: parsed["can_fix"],
171
+ reason: parsed["reason"],
172
+ root_causes: parsed["root_causes"] || [],
173
+ fixes: parsed["fixes"] || []
174
+ }
175
+ rescue JSON::ParserError => e
176
+ Aidp.log_error("ci_fix_processor", "Failed to parse AI response", error: e.message, content: content)
177
+ {can_fix: false, reason: "Failed to parse AI analysis"}
178
+ rescue => e
179
+ Aidp.log_error("ci_fix_processor", "AI analysis failed", error: e.message)
180
+ {can_fix: false, reason: "AI analysis error: #{e.message}"}
181
+ end
182
+
183
+ def ci_fix_system_prompt
184
+ <<~PROMPT
185
+ You are an expert CI/CD troubleshooter. Your task is to analyze CI failures and propose fixes.
186
+
187
+ Analyze the provided CI failure information and respond in JSON format:
188
+ {
189
+ "can_fix": true/false,
190
+ "reason": "Brief explanation of why you can or cannot fix this",
191
+ "root_causes": ["List of identified root causes"],
192
+ "fixes": [
193
+ {
194
+ "file": "path/to/file",
195
+ "action": "edit|create|delete",
196
+ "content": "Full file content after fix (for create/edit)",
197
+ "description": "What this fix does"
198
+ }
199
+ ]
200
+ }
201
+
202
+ Only propose fixes if you are confident they will resolve the issue.
203
+ Common CI failures you can fix:
204
+ - Linting errors (formatting, style violations)
205
+ - Simple test failures (typos, missing imports, incorrect assertions)
206
+ - Dependency issues (missing packages in manifest)
207
+ - Configuration errors (incorrect paths, missing env vars)
208
+
209
+ DO NOT attempt to fix:
210
+ - Complex logic errors requiring domain knowledge
211
+ - Failing integration tests that may indicate real bugs
212
+ - Security scan failures
213
+ - Performance regression issues
214
+ PROMPT
215
+ end
216
+
217
+ def build_ci_analysis_prompt(pr_data:, failures:)
218
+ <<~PROMPT
219
+ Analyze these CI failures for PR ##{pr_data[:number]}: #{pr_data[:title]}
220
+
221
+ **PR Description:**
222
+ #{pr_data[:body]}
223
+
224
+ **Failed Checks:**
225
+ #{failures.map { |f| "- #{f[:name]}: #{f[:conclusion]}\n Output: #{f[:output].inspect}" }.join("\n")}
226
+
227
+ Please analyze these failures and propose fixes if possible.
228
+ PROMPT
229
+ end
230
+
231
+ def detect_default_provider
232
+ config_manager = Aidp::Harness::ConfigManager.new(@project_dir)
233
+ config_manager.default_provider || "anthropic"
234
+ rescue
235
+ "anthropic"
236
+ end
237
+
238
+ def extract_json(text)
239
+ # Try to extract JSON from code fences or find JSON object
240
+ # Avoid regex to prevent ReDoS - use simple string operations
241
+ return text if text.start_with?("{") && text.end_with?("}")
242
+
243
+ # Extract from code fence using string operations
244
+ fence_start = text.index("```json")
245
+ if fence_start
246
+ json_start = text.index("{", fence_start)
247
+ fence_end = text.index("```", fence_start + 7)
248
+ if json_start && fence_end && json_start < fence_end
249
+ json_end = text.rindex("}", fence_end - 1)
250
+ return text[json_start..json_end] if json_end && json_end > json_start
251
+ end
252
+ end
253
+
254
+ # Find JSON object using string operations
255
+ first_brace = text.index("{")
256
+ last_brace = text.rindex("}")
257
+ if first_brace && last_brace && last_brace > first_brace
258
+ text[first_brace..last_brace]
259
+ else
260
+ text
261
+ end
262
+ end
263
+
264
+ def checkout_pr_branch(pr_data)
265
+ head_ref = pr_data[:head_ref]
266
+
267
+ Dir.chdir(@project_dir) do
268
+ # Fetch latest
269
+ run_git(%w[fetch origin])
270
+
271
+ # Checkout the PR branch
272
+ run_git(["checkout", head_ref])
273
+
274
+ # Pull latest changes
275
+ run_git(%w[pull --ff-only], allow_failure: true)
276
+ end
277
+
278
+ display_message("đŸŒŋ Checked out branch: #{head_ref}", type: :info)
279
+ end
280
+
281
+ def apply_fixes(fixes)
282
+ fixes.each do |fix|
283
+ file_path = File.join(@project_dir, fix["file"])
284
+
285
+ case fix["action"]
286
+ when "create", "edit"
287
+ FileUtils.mkdir_p(File.dirname(file_path))
288
+ File.write(file_path, fix["content"])
289
+ display_message(" ✓ #{fix["action"]} #{fix["file"]}", type: :muted) if @verbose
290
+ when "delete"
291
+ File.delete(file_path) if File.exist?(file_path)
292
+ display_message(" ✓ Deleted #{fix["file"]}", type: :muted) if @verbose
293
+ else
294
+ display_message(" âš ī¸ Unknown action: #{fix["action"]} for #{fix["file"]}", type: :warn)
295
+ end
296
+ end
297
+ end
298
+
299
+ def commit_and_push(pr_data, analysis)
300
+ Dir.chdir(@project_dir) do
301
+ # Check if there are changes
302
+ status_output = run_git(%w[status --porcelain])
303
+ if status_output.strip.empty?
304
+ display_message("â„šī¸ No changes to commit after applying fixes.", type: :muted)
305
+ return false
306
+ end
307
+
308
+ # Stage all changes
309
+ run_git(%w[add -A])
310
+
311
+ # Create commit
312
+ commit_message = build_commit_message(pr_data, analysis)
313
+ run_git(["commit", "-m", commit_message])
314
+
315
+ display_message("💾 Created commit: #{commit_message.lines.first.strip}", type: :info)
316
+
317
+ # Push to origin
318
+ head_ref = pr_data[:head_ref]
319
+ run_git(["push", "origin", head_ref])
320
+
321
+ display_message("âŦ†ī¸ Pushed fixes to #{head_ref}", type: :success)
322
+ true
323
+ end
324
+ end
325
+
326
+ def build_commit_message(pr_data, analysis)
327
+ root_causes = analysis[:root_causes] || []
328
+ fixes_description = analysis[:fixes]&.map { |f| f["description"] }&.join(", ") || "CI failures"
329
+
330
+ message = "fix: resolve CI failures for PR ##{pr_data[:number]}\n\n"
331
+ message += "Root causes:\n"
332
+ root_causes.each { |cause| message += "- #{cause}\n" }
333
+ message += "\nFixes: #{fixes_description}\n"
334
+ message += "\nCo-authored-by: AIDP CI Fixer <ai@aidp.dev>"
335
+
336
+ message
337
+ end
338
+
339
+ def handle_success(pr:, fix_result:)
340
+ comment = <<~COMMENT
341
+ #{COMMENT_HEADER}
342
+
343
+ ✅ Successfully analyzed and fixed CI failures!
344
+
345
+ **Root Causes:**
346
+ #{fix_result[:analysis][:root_causes].map { |c| "- #{c}" }.join("\n")}
347
+
348
+ **Applied Fixes:**
349
+ #{fix_result[:analysis][:fixes].map { |f| "- #{f["file"]}: #{f["description"]}" }.join("\n")}
350
+
351
+ The fixes have been committed and pushed to this PR. CI should re-run automatically.
352
+ COMMENT
353
+
354
+ @repository_client.post_comment(pr[:number], comment)
355
+ @state_store.record_ci_fix(pr[:number], {
356
+ status: "completed",
357
+ timestamp: Time.now.utc.iso8601,
358
+ root_causes: fix_result[:analysis][:root_causes],
359
+ fixes_count: fix_result[:analysis][:fixes].length
360
+ })
361
+
362
+ display_message("🎉 Posted success comment for PR ##{pr[:number]}", type: :success)
363
+
364
+ # Remove label after successful fix
365
+ begin
366
+ @repository_client.remove_labels(pr[:number], @ci_fix_label)
367
+ display_message("đŸˇī¸ Removed '#{@ci_fix_label}' label after successful fix", type: :info)
368
+ rescue => e
369
+ display_message("âš ī¸ Failed to remove CI fix label: #{e.message}", type: :warn)
370
+ end
371
+ end
372
+
373
+ def handle_failure(pr:, fix_result:)
374
+ reason = fix_result[:reason] || fix_result[:error] || "Unknown error"
375
+
376
+ analysis_section = if fix_result[:analysis]
377
+ "**Analysis:**\n#{fix_result[:analysis][:root_causes]&.map { |c| "- #{c}" }&.join("\n")}"
378
+ else
379
+ ""
380
+ end
381
+
382
+ comment = <<~COMMENT
383
+ #{COMMENT_HEADER}
384
+
385
+ âš ī¸ Could not automatically fix CI failures.
386
+
387
+ **Reason:** #{reason}
388
+
389
+ #{analysis_section}
390
+
391
+ Please review the CI failures manually. You may need to:
392
+ 1. Check the full CI logs for more context
393
+ 2. Run tests locally to reproduce the issue
394
+ 3. Consult with your team if the failures indicate a deeper problem
395
+
396
+ You can retry the automated fix by re-adding the `#{@ci_fix_label}` label after making changes.
397
+ COMMENT
398
+
399
+ @repository_client.post_comment(pr[:number], comment)
400
+ @state_store.record_ci_fix(pr[:number], {
401
+ status: "failed",
402
+ timestamp: Time.now.utc.iso8601,
403
+ reason: reason
404
+ })
405
+
406
+ display_message("âš ī¸ Posted failure comment for PR ##{pr[:number]}", type: :warn)
407
+ end
408
+
409
+ def post_success_comment(pr_data)
410
+ comment = <<~COMMENT
411
+ #{COMMENT_HEADER}
412
+
413
+ ✅ CI is already passing! No fixes needed.
414
+
415
+ All checks are green for this PR.
416
+ COMMENT
417
+
418
+ @repository_client.post_comment(pr_data[:number], comment)
419
+ end
420
+
421
+ def run_git(args, allow_failure: false)
422
+ stdout, stderr, status = Open3.capture3("git", *Array(args))
423
+ raise "git #{args.join(" ")} failed: #{stderr.strip}" unless status.success? || allow_failure
424
+ stdout
425
+ end
426
+
427
+ def log_ci_fix(pr_number, fix_result)
428
+ log_dir = File.join(@project_dir, ".aidp", "logs", "pr_reviews")
429
+ FileUtils.mkdir_p(log_dir)
430
+
431
+ log_file = File.join(log_dir, "ci_fix_#{pr_number}_#{Time.now.utc.strftime("%Y%m%d_%H%M%S")}.json")
432
+
433
+ log_data = {
434
+ pr_number: pr_number,
435
+ timestamp: Time.now.utc.iso8601,
436
+ success: fix_result[:success],
437
+ analysis: fix_result[:analysis],
438
+ error: fix_result[:error]
439
+ }
440
+
441
+ File.write(log_file, JSON.pretty_generate(log_data))
442
+ display_message("📝 CI fix log saved to #{log_file}", type: :muted) if @verbose
443
+ rescue => e
444
+ display_message("âš ī¸ Failed to save CI fix log: #{e.message}", type: :warn)
445
+ end
446
+ end
447
+ end
448
+ end
@@ -49,7 +49,10 @@ module Aidp
49
49
  display_message("🧠 Generating plan for issue ##{number} (#{issue[:title]})", type: :info)
50
50
  plan_data = @plan_generator.generate(issue)
51
51
 
52
- comment_body = build_comment(issue: issue, plan: plan_data)
52
+ # Fetch the user who added the most recent label
53
+ label_actor = @repository_client.most_recent_label_actor(number)
54
+
55
+ comment_body = build_comment(issue: issue, plan: plan_data, label_actor: label_actor)
53
56
  @repository_client.post_comment(number, comment_body)
54
57
 
55
58
  display_message("đŸ’Ŧ Posted plan comment for issue ##{number}", type: :success)
@@ -82,7 +85,7 @@ module Aidp
82
85
  end
83
86
  end
84
87
 
85
- def build_comment(issue:, plan:)
88
+ def build_comment(issue:, plan:, label_actor: nil)
86
89
  summary = plan[:summary].to_s.strip
87
90
  tasks = Array(plan[:tasks])
88
91
  questions = Array(plan[:questions])
@@ -91,6 +94,13 @@ module Aidp
91
94
  parts = []
92
95
  parts << COMMENT_HEADER
93
96
  parts << ""
97
+
98
+ # Tag the label actor if available
99
+ if label_actor
100
+ parts << "cc @#{label_actor}"
101
+ parts << ""
102
+ end
103
+
94
104
  parts << "**Issue**: [##{issue[:number]}](#{issue[:url]})"
95
105
  parts << "**Title**: #{issue[:title]}"
96
106
  parts << ""