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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +72 -7
  3. data/lib/aidp/analyze/error_handler.rb +11 -0
  4. data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
  5. data/lib/aidp/auto_update/checkpoint.rb +178 -0
  6. data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
  7. data/lib/aidp/auto_update/coordinator.rb +204 -0
  8. data/lib/aidp/auto_update/errors.rb +17 -0
  9. data/lib/aidp/auto_update/failure_tracker.rb +162 -0
  10. data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
  11. data/lib/aidp/auto_update/update_check.rb +106 -0
  12. data/lib/aidp/auto_update/update_logger.rb +143 -0
  13. data/lib/aidp/auto_update/update_policy.rb +109 -0
  14. data/lib/aidp/auto_update/version_detector.rb +144 -0
  15. data/lib/aidp/auto_update.rb +52 -0
  16. data/lib/aidp/cli.rb +165 -1
  17. data/lib/aidp/execute/work_loop_runner.rb +225 -55
  18. data/lib/aidp/harness/config_loader.rb +20 -11
  19. data/lib/aidp/harness/config_schema.rb +80 -8
  20. data/lib/aidp/harness/configuration.rb +73 -2
  21. data/lib/aidp/harness/filter_strategy.rb +45 -0
  22. data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
  23. data/lib/aidp/harness/output_filter.rb +136 -0
  24. data/lib/aidp/harness/provider_factory.rb +2 -0
  25. data/lib/aidp/harness/provider_manager.rb +18 -3
  26. data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
  27. data/lib/aidp/harness/test_runner.rb +165 -27
  28. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
  29. data/lib/aidp/logger.rb +35 -5
  30. data/lib/aidp/message_display.rb +56 -2
  31. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
  32. data/lib/aidp/provider_manager.rb +2 -0
  33. data/lib/aidp/providers/kilocode.rb +202 -0
  34. data/lib/aidp/safe_directory.rb +10 -3
  35. data/lib/aidp/setup/provider_registry.rb +15 -0
  36. data/lib/aidp/setup/wizard.rb +12 -4
  37. data/lib/aidp/skills/composer.rb +4 -0
  38. data/lib/aidp/skills/loader.rb +3 -1
  39. data/lib/aidp/storage/csv_storage.rb +9 -3
  40. data/lib/aidp/storage/file_manager.rb +8 -2
  41. data/lib/aidp/storage/json_storage.rb +9 -3
  42. data/lib/aidp/version.rb +1 -1
  43. data/lib/aidp/watch/build_processor.rb +106 -17
  44. data/lib/aidp/watch/change_request_processor.rb +659 -0
  45. data/lib/aidp/watch/ci_fix_processor.rb +448 -0
  46. data/lib/aidp/watch/plan_processor.rb +81 -8
  47. data/lib/aidp/watch/repository_client.rb +465 -20
  48. data/lib/aidp/watch/review_processor.rb +266 -0
  49. data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
  50. data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
  51. data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
  52. data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
  53. data/lib/aidp/watch/runner.rb +222 -0
  54. data/lib/aidp/watch/state_store.rb +99 -1
  55. data/lib/aidp/workstream_executor.rb +5 -2
  56. data/lib/aidp.rb +5 -0
  57. data/templates/aidp.yml.example +53 -0
  58. metadata +25 -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
@@ -41,18 +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
- comment_body = build_comment(issue: issue, plan: plan_data)
53
- @repository_client.post_comment(number, comment_body)
54
+ # Fetch the user who added the most recent label
55
+ label_actor = @repository_client.most_recent_label_actor(number)
56
+
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
54
83
 
55
- display_message("đŸ’Ŧ Posted plan comment for issue ##{number}", type: :success)
56
84
  @state_store.record_plan(number, plan_data.merge(comment_body: comment_body, comment_hint: COMMENT_HEADER))
57
85
 
58
86
  # Update labels: remove plan trigger, add appropriate status label
@@ -61,6 +89,31 @@ module Aidp
61
89
 
62
90
  private
63
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
+
64
117
  def update_labels_after_plan(number, plan_data)
65
118
  questions = Array(plan_data[:questions])
66
119
  has_questions = questions.any? && !questions.all? { |q| q.to_s.strip.empty? }
@@ -82,7 +135,7 @@ module Aidp
82
135
  end
83
136
  end
84
137
 
85
- def build_comment(issue:, plan:)
138
+ def build_comment(issue:, plan:, label_actor: nil, archived_content: nil)
86
139
  summary = plan[:summary].to_s.strip
87
140
  tasks = Array(plan[:tasks])
88
141
  questions = Array(plan[:questions])
@@ -91,17 +144,37 @@ module Aidp
91
144
  parts = []
92
145
  parts << COMMENT_HEADER
93
146
  parts << ""
147
+
148
+ # Tag the label actor if available
149
+ if label_actor
150
+ parts << "cc @#{label_actor}"
151
+ parts << ""
152
+ end
153
+
94
154
  parts << "**Issue**: [##{issue[:number]}](#{issue[:url]})"
95
155
  parts << "**Title**: #{issue[:title]}"
96
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 -->"
97
165
  parts << "### Plan Summary"
98
166
  parts << (summary.empty? ? "_No summary generated_" : summary)
167
+ parts << "<!-- PLAN_SUMMARY_END -->"
99
168
  parts << ""
169
+ parts << "<!-- PLAN_TASKS_START -->"
100
170
  parts << "### Proposed Tasks"
101
171
  parts << format_bullets(tasks, placeholder: "_Pending task breakdown_")
172
+ parts << "<!-- PLAN_TASKS_END -->"
102
173
  parts << ""
174
+ parts << "<!-- CLARIFYING_QUESTIONS_START -->"
103
175
  parts << "### Clarifying Questions"
104
176
  parts << format_numbered(questions, placeholder: "_No questions identified_")
177
+ parts << "<!-- CLARIFYING_QUESTIONS_END -->"
105
178
  parts << ""
106
179
 
107
180
  # Add instructions based on whether there are questions