aidp 0.32.0 → 0.34.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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/lib/aidp/analyze/feature_analyzer.rb +322 -320
  4. data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
  5. data/lib/aidp/auto_update/coordinator.rb +97 -7
  6. data/lib/aidp/auto_update.rb +0 -12
  7. data/lib/aidp/cli/devcontainer_commands.rb +0 -5
  8. data/lib/aidp/cli/eval_command.rb +399 -0
  9. data/lib/aidp/cli/harness_command.rb +1 -1
  10. data/lib/aidp/cli/security_command.rb +416 -0
  11. data/lib/aidp/cli/tools_command.rb +6 -4
  12. data/lib/aidp/cli.rb +172 -4
  13. data/lib/aidp/comment_consolidator.rb +78 -0
  14. data/lib/aidp/concurrency/exec.rb +3 -0
  15. data/lib/aidp/concurrency.rb +0 -3
  16. data/lib/aidp/config.rb +113 -1
  17. data/lib/aidp/config_paths.rb +91 -0
  18. data/lib/aidp/daemon/runner.rb +8 -4
  19. data/lib/aidp/errors.rb +134 -0
  20. data/lib/aidp/evaluations/context_capture.rb +205 -0
  21. data/lib/aidp/evaluations/evaluation_record.rb +114 -0
  22. data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
  23. data/lib/aidp/evaluations.rb +23 -0
  24. data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
  25. data/lib/aidp/execute/interactive_repl.rb +6 -2
  26. data/lib/aidp/execute/prompt_evaluator.rb +359 -0
  27. data/lib/aidp/execute/repl_macros.rb +100 -1
  28. data/lib/aidp/execute/work_loop_runner.rb +719 -58
  29. data/lib/aidp/execute/work_loop_state.rb +4 -1
  30. data/lib/aidp/execute/workflow_selector.rb +3 -0
  31. data/lib/aidp/harness/ai_decision_engine.rb +79 -0
  32. data/lib/aidp/harness/ai_filter_factory.rb +285 -0
  33. data/lib/aidp/harness/capability_registry.rb +2 -0
  34. data/lib/aidp/harness/condition_detector.rb +3 -0
  35. data/lib/aidp/harness/config_loader.rb +3 -0
  36. data/lib/aidp/harness/config_schema.rb +97 -1
  37. data/lib/aidp/harness/config_validator.rb +1 -1
  38. data/lib/aidp/harness/configuration.rb +61 -5
  39. data/lib/aidp/harness/enhanced_runner.rb +14 -11
  40. data/lib/aidp/harness/error_handler.rb +3 -0
  41. data/lib/aidp/harness/filter_definition.rb +212 -0
  42. data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
  43. data/lib/aidp/harness/output_filter.rb +50 -25
  44. data/lib/aidp/harness/output_filter_config.rb +129 -0
  45. data/lib/aidp/harness/provider_factory.rb +3 -0
  46. data/lib/aidp/harness/provider_manager.rb +96 -2
  47. data/lib/aidp/harness/runner.rb +5 -12
  48. data/lib/aidp/harness/state/persistence.rb +3 -0
  49. data/lib/aidp/harness/state_manager.rb +3 -0
  50. data/lib/aidp/harness/status_display.rb +28 -20
  51. data/lib/aidp/harness/test_runner.rb +179 -41
  52. data/lib/aidp/harness/thinking_depth_manager.rb +44 -28
  53. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
  54. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
  55. data/lib/aidp/harness/ui/error_handler.rb +3 -0
  56. data/lib/aidp/harness/ui/job_monitor.rb +4 -0
  57. data/lib/aidp/harness/ui/navigation/submenu.rb +2 -2
  58. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
  59. data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
  60. data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
  61. data/lib/aidp/harness/user_interface.rb +3 -0
  62. data/lib/aidp/loader.rb +195 -0
  63. data/lib/aidp/logger.rb +3 -0
  64. data/lib/aidp/message_display.rb +31 -0
  65. data/lib/aidp/metadata/compiler.rb +29 -17
  66. data/lib/aidp/metadata/query.rb +1 -1
  67. data/lib/aidp/metadata/scanner.rb +8 -1
  68. data/lib/aidp/metadata/tool_metadata.rb +13 -13
  69. data/lib/aidp/metadata/validator.rb +10 -0
  70. data/lib/aidp/metadata.rb +16 -0
  71. data/lib/aidp/pr_worktree_manager.rb +20 -8
  72. data/lib/aidp/provider_manager.rb +4 -7
  73. data/lib/aidp/providers/base.rb +2 -0
  74. data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
  75. data/lib/aidp/security/secrets_proxy.rb +328 -0
  76. data/lib/aidp/security/secrets_registry.rb +227 -0
  77. data/lib/aidp/security/trifecta_state.rb +220 -0
  78. data/lib/aidp/security/watch_mode_handler.rb +306 -0
  79. data/lib/aidp/security/work_loop_adapter.rb +277 -0
  80. data/lib/aidp/security.rb +56 -0
  81. data/lib/aidp/setup/wizard.rb +283 -11
  82. data/lib/aidp/skills.rb +0 -5
  83. data/lib/aidp/storage/csv_storage.rb +3 -0
  84. data/lib/aidp/style_guide/selector.rb +360 -0
  85. data/lib/aidp/tooling_detector.rb +283 -16
  86. data/lib/aidp/version.rb +1 -1
  87. data/lib/aidp/watch/auto_merger.rb +274 -0
  88. data/lib/aidp/watch/auto_pr_processor.rb +125 -7
  89. data/lib/aidp/watch/build_processor.rb +16 -1
  90. data/lib/aidp/watch/change_request_processor.rb +682 -150
  91. data/lib/aidp/watch/ci_fix_processor.rb +262 -4
  92. data/lib/aidp/watch/feedback_collector.rb +191 -0
  93. data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
  94. data/lib/aidp/watch/implementation_verifier.rb +142 -1
  95. data/lib/aidp/watch/plan_generator.rb +70 -13
  96. data/lib/aidp/watch/plan_processor.rb +12 -5
  97. data/lib/aidp/watch/projects_processor.rb +286 -0
  98. data/lib/aidp/watch/repository_client.rb +871 -22
  99. data/lib/aidp/watch/review_processor.rb +33 -6
  100. data/lib/aidp/watch/runner.rb +80 -29
  101. data/lib/aidp/watch/state_store.rb +233 -0
  102. data/lib/aidp/watch/sub_issue_creator.rb +221 -0
  103. data/lib/aidp/watch.rb +5 -7
  104. data/lib/aidp/workflows/guided_agent.rb +4 -0
  105. data/lib/aidp/workstream_cleanup.rb +0 -2
  106. data/lib/aidp/workstream_executor.rb +3 -4
  107. data/lib/aidp/worktree.rb +61 -12
  108. data/lib/aidp/worktree_branch_manager.rb +347 -101
  109. data/lib/aidp.rb +21 -106
  110. data/templates/implementation/iterative_implementation.md +46 -3
  111. metadata +91 -36
  112. data/lib/aidp/config/paths.rb +0 -131
@@ -10,6 +10,7 @@ require_relative "implementation_verifier"
10
10
  require_relative "reviewers/senior_dev_reviewer"
11
11
  require_relative "reviewers/security_reviewer"
12
12
  require_relative "reviewers/performance_reviewer"
13
+ require_relative "feedback_collector"
13
14
 
14
15
  module Aidp
15
16
  module Watch
@@ -81,13 +82,16 @@ module Aidp
81
82
  review_results: review_results,
82
83
  verification_result: verification_result
83
84
  )
84
- @repository_client.post_comment(number, comment_body)
85
+ comment_body_with_feedback = FeedbackCollector.append_feedback_prompt(comment_body)
86
+ result = @repository_client.post_comment(number, comment_body_with_feedback)
87
+ comment_id = result[:id] if result.is_a?(Hash)
85
88
 
86
89
  display_message("💬 Posted review comment for PR ##{number}", type: :success)
87
90
  @state_store.record_review(number, {
88
91
  timestamp: Time.now.utc.iso8601,
89
92
  reviewers: review_results.map { |r| r[:persona] },
90
- total_findings: review_results.sum { |r| r[:findings].length }
93
+ total_findings: review_results.sum { |r| r[:findings].length },
94
+ comment_id: comment_id
91
95
  })
92
96
 
93
97
  # Remove review label after processing
@@ -200,12 +204,35 @@ module Aidp
200
204
  else
201
205
  parts << "### ⚠️ Implementation Incomplete"
202
206
  parts << ""
203
- parts << "**This PR appears to be incomplete based on the linked issue requirements:**"
207
+ parts << "**This PR appears to be incomplete based on the linked issue requirements.**"
204
208
  parts << ""
205
- verification_result[:reasons]&.each do |reason|
206
- parts << "- #{reason}"
209
+
210
+ # Show the verification reasoning
211
+ if verification_result[:reason]
212
+ parts << "**Summary:** #{verification_result[:reason]}"
213
+ parts << ""
207
214
  end
208
- parts << ""
215
+
216
+ # Show missing requirements for implementers to address
217
+ if verification_result[:missing_items]&.any?
218
+ parts << "**Missing Requirements:**"
219
+ parts << ""
220
+ verification_result[:missing_items].each do |item|
221
+ parts << "- #{item}"
222
+ end
223
+ parts << ""
224
+ end
225
+
226
+ # Show additional work needed for implementers
227
+ if verification_result[:additional_work]&.any?
228
+ parts << "**Additional Work Needed:**"
229
+ parts << ""
230
+ verification_result[:additional_work].each do |work|
231
+ parts << "- #{work}"
232
+ end
233
+ parts << ""
234
+ end
235
+
209
236
  parts << "**Suggested Action:** Add the `aidp-request-changes` label if you'd like AIDP to help complete the implementation."
210
237
  end
211
238
  parts << ""
@@ -1,21 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "tty-prompt"
4
-
5
- require_relative "../message_display"
6
- require_relative "repository_client"
7
- require_relative "repository_safety_checker"
8
- require_relative "state_store"
4
+ require_relative "feedback_collector"
9
5
  require_relative "github_state_extractor"
10
- require_relative "plan_generator"
11
- require_relative "plan_processor"
12
- require_relative "build_processor"
13
- require_relative "../auto_update"
14
- require_relative "review_processor"
15
- require_relative "ci_fix_processor"
16
- require_relative "change_request_processor"
17
- require_relative "auto_processor"
18
- require_relative "auto_pr_processor"
19
6
 
20
7
  module Aidp
21
8
  module Watch
@@ -26,13 +13,18 @@ module Aidp
26
13
 
27
14
  DEFAULT_INTERVAL = 30
28
15
 
29
- def initialize(issues_url:, interval: DEFAULT_INTERVAL, provider_name: nil, gh_available: nil, project_dir: Dir.pwd, once: false, use_workstreams: true, prompt: TTY::Prompt.new, safety_config: {}, force: false, verbose: false)
16
+ # Expose for testability
17
+ attr_reader :post_detection_comments
18
+ attr_writer :last_update_check
19
+
20
+ def initialize(issues_url:, interval: DEFAULT_INTERVAL, provider_name: nil, gh_available: nil, project_dir: Dir.pwd, once: false, use_workstreams: true, prompt: TTY::Prompt.new, safety_config: {}, force: false, verbose: false, quiet: false)
30
21
  @prompt = prompt
31
22
  @interval = interval
32
23
  @once = once
33
24
  @project_dir = project_dir
34
25
  @force = force
35
26
  @verbose = verbose
27
+ @quiet = quiet
36
28
  @provider_name = provider_name
37
29
  @safety_config = safety_config
38
30
 
@@ -117,6 +109,12 @@ module Aidp
117
109
  safety_config: safety_config[:safety] || safety_config["safety"] || {},
118
110
  verbose: verbose
119
111
  )
112
+
113
+ @feedback_collector = FeedbackCollector.new(
114
+ repository_client: @repository_client,
115
+ state_store: @state_store,
116
+ project_dir: project_dir
117
+ )
120
118
  end
121
119
 
122
120
  def start
@@ -144,6 +142,7 @@ module Aidp
144
142
  process_cycle
145
143
  Aidp.log_debug("watch_runner", "poll_cycle.complete", once: @once, next_poll_in: @once ? nil : @interval)
146
144
  break if @once
145
+
147
146
  Aidp.log_debug("watch_runner", "poll_cycle.sleep", seconds: @interval)
148
147
  sleep @interval
149
148
  end
@@ -165,6 +164,7 @@ module Aidp
165
164
  process_ci_fix_triggers
166
165
  process_auto_pr_triggers
167
166
  process_change_request_triggers
167
+ collect_feedback
168
168
  end
169
169
 
170
170
  def process_plan_triggers
@@ -197,7 +197,8 @@ module Aidp
197
197
 
198
198
  # Check author authorization before processing
199
199
  unless @safety_checker.should_process_issue?(detailed, enforce: false)
200
- Aidp.log_debug("watch_runner", "plan_skip_unauthorized_author", issue: detailed[:number], author: detailed[:author])
200
+ Aidp.log_debug("watch_runner", "plan_skip_unauthorized_author", issue: detailed[:number],
201
+ author: detailed[:author])
201
202
  next
202
203
  end
203
204
 
@@ -253,7 +254,8 @@ module Aidp
253
254
 
254
255
  # Check author authorization before processing
255
256
  unless @safety_checker.should_process_issue?(detailed, enforce: false)
256
- Aidp.log_debug("watch_runner", "build_skip_unauthorized_author", issue: detailed[:number], author: detailed[:author])
257
+ Aidp.log_debug("watch_runner", "build_skip_unauthorized_author", issue: detailed[:number],
258
+ author: detailed[:author])
257
259
  next
258
260
  end
259
261
 
@@ -276,7 +278,8 @@ module Aidp
276
278
  begin
277
279
  @repository_client.remove_labels(detailed[:number], GitHubStateExtractor::IN_PROGRESS_LABEL)
278
280
  rescue => e
279
- Aidp.log_warn("watch_runner", "failed_to_remove_in_progress_label", issue: detailed[:number], error: e.message)
281
+ Aidp.log_warn("watch_runner", "failed_to_remove_in_progress_label", issue: detailed[:number],
282
+ error: e.message)
280
283
  end
281
284
  end
282
285
  end
@@ -296,7 +299,8 @@ module Aidp
296
299
 
297
300
  issues.each do |issue|
298
301
  unless issue_has_label?(issue, auto_label)
299
- Aidp.log_debug("watch_runner", "auto_issue_skip_label_mismatch", issue: issue[:number], labels: issue[:labels])
302
+ Aidp.log_debug("watch_runner", "auto_issue_skip_label_mismatch", issue: issue[:number],
303
+ labels: issue[:labels])
300
304
  next
301
305
  end
302
306
 
@@ -315,7 +319,8 @@ module Aidp
315
319
 
316
320
  # Check author authorization before processing
317
321
  unless @safety_checker.should_process_issue?(detailed, enforce: false)
318
- Aidp.log_debug("watch_runner", "auto_issue_skip_unauthorized_author", issue: detailed[:number], author: detailed[:author])
322
+ Aidp.log_debug("watch_runner", "auto_issue_skip_unauthorized_author", issue: detailed[:number],
323
+ author: detailed[:author])
319
324
  next
320
325
  end
321
326
 
@@ -361,7 +366,8 @@ module Aidp
361
366
 
362
367
  # Check author authorization before processing
363
368
  unless @safety_checker.should_process_issue?(detailed, enforce: false)
364
- Aidp.log_debug("watch_runner", "review_skip_unauthorized_author", pr: detailed[:number], author: detailed[:author])
369
+ Aidp.log_debug("watch_runner", "review_skip_unauthorized_author", pr: detailed[:number],
370
+ author: detailed[:author])
365
371
  next
366
372
  end
367
373
 
@@ -398,7 +404,8 @@ module Aidp
398
404
 
399
405
  # Check author authorization before processing
400
406
  unless @safety_checker.should_process_issue?(detailed, enforce: false)
401
- Aidp.log_debug("watch_runner", "auto_pr_skip_unauthorized_author", pr: detailed[:number], author: detailed[:author])
407
+ Aidp.log_debug("watch_runner", "auto_pr_skip_unauthorized_author", pr: detailed[:number],
408
+ author: detailed[:author])
402
409
  next
403
410
  end
404
411
 
@@ -434,7 +441,8 @@ module Aidp
434
441
 
435
442
  # Check author authorization before processing
436
443
  unless @safety_checker.should_process_issue?(detailed, enforce: false)
437
- Aidp.log_debug("watch_runner", "ci_fix_skip_unauthorized_author", pr: detailed[:number], author: detailed[:author])
444
+ Aidp.log_debug("watch_runner", "ci_fix_skip_unauthorized_author", pr: detailed[:number],
445
+ author: detailed[:author])
438
446
  next
439
447
  end
440
448
 
@@ -470,7 +478,8 @@ module Aidp
470
478
 
471
479
  # Check author authorization before processing
472
480
  unless @safety_checker.should_process_issue?(detailed, enforce: false)
473
- Aidp.log_debug("watch_runner", "change_request_skip_unauthorized_author", pr: detailed[:number], author: detailed[:author])
481
+ Aidp.log_debug("watch_runner", "change_request_skip_unauthorized_author", pr: detailed[:number],
482
+ author: detailed[:author])
474
483
  next
475
484
  end
476
485
 
@@ -522,14 +531,22 @@ module Aidp
522
531
  return unless @auto_update_coordinator.policy.enabled
523
532
  return unless time_for_update_check?
524
533
 
534
+ @last_update_check = Time.now
525
535
  update_check = @auto_update_coordinator.check_for_update
526
536
 
527
537
  if update_check.should_update?
528
- display_message("🔄 Update available: #{update_check.current_version} → #{update_check.available_version}", type: :highlight)
529
- display_message(" Saving checkpoint and initiating update...", type: :muted)
530
-
531
- initiate_update(update_check)
532
- # Never returns - exits with code 75
538
+ display_message("🔄 Update available: #{update_check.current_version} → #{update_check.available_version}",
539
+ type: :highlight)
540
+
541
+ # Prefer hot reloading if available (Zeitwerk enabled with reloading)
542
+ if @auto_update_coordinator.hot_reload_available?
543
+ perform_hot_reload(update_check)
544
+ else
545
+ # Fall back to checkpoint + exit approach
546
+ display_message(" Saving checkpoint and initiating update...", type: :muted)
547
+ initiate_update(update_check)
548
+ # Never returns - exits with code 75
549
+ end
533
550
  end
534
551
  rescue Aidp::AutoUpdate::UpdateLoopError => e
535
552
  # Restart loop detected - disable auto-update
@@ -540,6 +557,25 @@ module Aidp
540
557
  Aidp.log_error("watch_runner", "update_check_failed", error: e.message)
541
558
  end
542
559
 
560
+ # Perform hot code reload without restarting
561
+ # @param update_check [Aidp::AutoUpdate::UpdateCheck] Update check result
562
+ def perform_hot_reload(update_check)
563
+ display_message(" Performing hot code reload (no restart needed)...", type: :muted)
564
+
565
+ if @auto_update_coordinator.hot_reload_update(update_check)
566
+ display_message("✨ Hot reload complete! Now running #{Aidp::VERSION}", type: :success)
567
+ Aidp.log_info("watch_runner", "hot_reload_success",
568
+ version: Aidp::VERSION)
569
+ else
570
+ display_message("⚠️ Hot reload skipped (no update needed)", type: :muted)
571
+ end
572
+ rescue Aidp::AutoUpdate::UpdateError => e
573
+ display_message("⚠️ Hot reload failed: #{e.message}", type: :warning)
574
+ display_message(" Falling back to checkpoint + restart...", type: :muted)
575
+ # Fall back to cold restart
576
+ initiate_update(update_check)
577
+ end
578
+
543
579
  # Determine if it's time to check for updates
544
580
  # @return [Boolean]
545
581
  def time_for_update_check?
@@ -633,6 +669,21 @@ module Aidp
633
669
  name.casecmp(label).zero?
634
670
  end
635
671
  end
672
+
673
+ # Collect feedback from reactions on tracked comments
674
+ def collect_feedback
675
+ new_evaluations = @feedback_collector.collect_feedback
676
+ return if new_evaluations.empty?
677
+
678
+ Aidp.log_info("watch_runner", "feedback_collected",
679
+ count: new_evaluations.size,
680
+ evaluations: new_evaluations.map { |e| {id: e[:id], rating: e[:rating]} })
681
+
682
+ display_message("📊 Collected #{new_evaluations.size} new feedback evaluation(s)", type: :info) if @verbose
683
+ rescue => e
684
+ Aidp.log_error("watch_runner", "feedback_collection_failed", error: e.message)
685
+ display_message("⚠️ Feedback collection failed: #{e.message}", type: :warn) if @verbose
686
+ end
636
687
  end
637
688
  end
638
689
  end
@@ -189,6 +189,214 @@ module Aidp
189
189
  save!
190
190
  end
191
191
 
192
+ # Feedback tracking methods - track comments for reaction-based evaluations
193
+
194
+ # Get all tracked comments with their metadata for feedback collection
195
+ # @return [Array<Hash>] List of comment info hashes
196
+ def tracked_comments
197
+ comments = []
198
+
199
+ # Collect from plans
200
+ plans.each do |issue_number, data|
201
+ next unless data["comment_id"]
202
+ comments << {
203
+ comment_id: data["comment_id"],
204
+ processor_type: "plan",
205
+ number: issue_number.to_i,
206
+ posted_at: data["posted_at"]
207
+ }
208
+ end
209
+
210
+ # Collect from reviews (if they store comment_id)
211
+ reviews.each do |pr_number, data|
212
+ next unless data["comment_id"]
213
+ comments << {
214
+ comment_id: data["comment_id"],
215
+ processor_type: "review",
216
+ number: pr_number.to_i,
217
+ posted_at: data["timestamp"]
218
+ }
219
+ end
220
+
221
+ # Collect from builds (if they store comment_id)
222
+ builds.each do |issue_number, data|
223
+ next unless data["comment_id"]
224
+ comments << {
225
+ comment_id: data["comment_id"],
226
+ processor_type: "build",
227
+ number: issue_number.to_i,
228
+ posted_at: data["updated_at"]
229
+ }
230
+ end
231
+
232
+ # Collect from feedback_comments (explicitly tracked)
233
+ feedback_comments.each do |key, data|
234
+ comments << {
235
+ comment_id: data["comment_id"],
236
+ processor_type: data["processor_type"],
237
+ number: data["number"].to_i,
238
+ posted_at: data["posted_at"]
239
+ }
240
+ end
241
+
242
+ comments
243
+ end
244
+
245
+ # Track a comment for feedback collection
246
+ # @param comment_id [Integer, String] GitHub comment ID
247
+ # @param processor_type [String] Type of processor (plan, review, build, etc.)
248
+ # @param number [Integer] Issue or PR number
249
+ def track_comment_for_feedback(comment_id:, processor_type:, number:)
250
+ key = "#{processor_type}_#{number}"
251
+ feedback_comments[key] = {
252
+ "comment_id" => comment_id.to_s,
253
+ "processor_type" => processor_type,
254
+ "number" => number,
255
+ "posted_at" => Time.now.utc.iso8601
256
+ }
257
+ save!
258
+ end
259
+
260
+ # Get IDs of reactions already processed for a comment
261
+ # @param comment_id [Integer, String] GitHub comment ID
262
+ # @return [Array<Integer>] List of processed reaction IDs
263
+ def processed_reaction_ids(comment_id)
264
+ data = processed_reactions[comment_id.to_s]
265
+ return [] unless data
266
+ data["reaction_ids"] || []
267
+ end
268
+
269
+ # Mark a reaction as processed
270
+ # @param comment_id [Integer, String] GitHub comment ID
271
+ # @param reaction_id [Integer] GitHub reaction ID
272
+ def mark_reaction_processed(comment_id, reaction_id)
273
+ key = comment_id.to_s
274
+ processed_reactions[key] ||= {"reaction_ids" => [], "last_checked" => nil}
275
+ processed_reactions[key]["reaction_ids"] << reaction_id unless processed_reactions[key]["reaction_ids"].include?(reaction_id)
276
+ processed_reactions[key]["last_checked"] = Time.now.utc.iso8601
277
+ save!
278
+ end
279
+
280
+ # Auto PR tracking methods - for aidp-auto label on PRs
281
+ # Tracks iteration counts to enforce iteration cap
282
+
283
+ # Get the current iteration count for an auto PR
284
+ # @param pr_number [Integer] PR number
285
+ # @return [Integer] Current iteration count (0 if not tracked)
286
+ def auto_pr_iteration_count(pr_number)
287
+ data = auto_prs[pr_number.to_s]
288
+ return 0 unless data
289
+ data["iteration"] || 0
290
+ end
291
+
292
+ # Get full auto PR data
293
+ # @param pr_number [Integer] PR number
294
+ # @return [Hash, nil] Auto PR tracking data
295
+ def auto_pr_data(pr_number)
296
+ auto_prs[pr_number.to_s]
297
+ end
298
+
299
+ # Record an auto PR iteration
300
+ # @param pr_number [Integer] PR number
301
+ # @param data [Hash] Additional data to store
302
+ # @return [Integer] New iteration count
303
+ def record_auto_pr_iteration(pr_number, data = {})
304
+ key = pr_number.to_s
305
+ existing = auto_prs[key] || {}
306
+ iteration = (existing["iteration"] || 0) + 1
307
+
308
+ auto_prs[key] = {
309
+ "iteration" => iteration,
310
+ "last_processed_at" => Time.now.utc.iso8601,
311
+ "status" => data[:status] || "in_progress",
312
+ "metadata" => stringify_keys(data[:metadata] || {})
313
+ }.merge(stringify_keys(data.except(:status, :metadata)))
314
+
315
+ save!
316
+ iteration
317
+ end
318
+
319
+ # Mark an auto PR as completed (ready for human review)
320
+ # @param pr_number [Integer] PR number
321
+ # @param data [Hash] Additional completion data
322
+ def complete_auto_pr(pr_number, data = {})
323
+ key = pr_number.to_s
324
+ existing = auto_prs[key] || {}
325
+
326
+ auto_prs[key] = existing.merge({
327
+ "status" => "completed",
328
+ "completed_at" => Time.now.utc.iso8601
329
+ }).merge(stringify_keys(data))
330
+
331
+ save!
332
+ end
333
+
334
+ # Check if an auto PR has reached the iteration cap
335
+ # @param pr_number [Integer] PR number
336
+ # @param cap [Integer] Maximum iterations allowed
337
+ # @return [Boolean] True if cap reached
338
+ def auto_pr_cap_reached?(pr_number, cap:)
339
+ auto_pr_iteration_count(pr_number) >= cap
340
+ end
341
+
342
+ # Project tracking methods
343
+ def project_item_id(issue_number)
344
+ projects[issue_number.to_s]&.dig("item_id")
345
+ end
346
+
347
+ def record_project_item_id(issue_number, item_id)
348
+ projects[issue_number.to_s] ||= {}
349
+ projects[issue_number.to_s]["item_id"] = item_id
350
+ projects[issue_number.to_s]["synced_at"] = Time.now.utc.iso8601
351
+ save!
352
+ end
353
+
354
+ def project_sync_data(issue_number)
355
+ projects[issue_number.to_s] || {}
356
+ end
357
+
358
+ def record_project_sync(issue_number, data)
359
+ projects[issue_number.to_s] ||= {}
360
+ projects[issue_number.to_s].merge!(stringify_keys(data))
361
+ projects[issue_number.to_s]["synced_at"] = Time.now.utc.iso8601
362
+ save!
363
+ end
364
+
365
+ # Sub-issue tracking methods
366
+ def sub_issues(parent_number)
367
+ hierarchies[parent_number.to_s]&.dig("sub_issues") || []
368
+ end
369
+
370
+ def parent_issue(sub_issue_number)
371
+ hierarchies[sub_issue_number.to_s]&.dig("parent")
372
+ end
373
+
374
+ def record_sub_issues(parent_number, sub_issue_numbers)
375
+ hierarchies[parent_number.to_s] ||= {}
376
+ hierarchies[parent_number.to_s]["sub_issues"] = Array(sub_issue_numbers)
377
+ hierarchies[parent_number.to_s]["created_at"] = Time.now.utc.iso8601
378
+
379
+ # Also record reverse mapping
380
+ sub_issue_numbers.each do |sub_number|
381
+ hierarchies[sub_number.to_s] ||= {}
382
+ hierarchies[sub_number.to_s]["parent"] = parent_number
383
+ end
384
+
385
+ save!
386
+ end
387
+
388
+ def blocking_status(issue_number)
389
+ # Check if this issue is blocked by any open sub-issues
390
+ sub_issue_numbers = sub_issues(issue_number)
391
+ return {blocked: false, blockers: []} if sub_issue_numbers.empty?
392
+
393
+ {
394
+ blocked: true,
395
+ blockers: sub_issue_numbers,
396
+ blocker_count: sub_issue_numbers.size
397
+ }
398
+ end
399
+
192
400
  private
193
401
 
194
402
  def ensure_directory
@@ -221,6 +429,11 @@ module Aidp
221
429
  base["ci_fixes"] ||= {}
222
430
  base["change_requests"] ||= {}
223
431
  base["detection_comments"] ||= {}
432
+ base["feedback_comments"] ||= {}
433
+ base["processed_reactions"] ||= {}
434
+ base["auto_prs"] ||= {}
435
+ base["projects"] ||= {}
436
+ base["hierarchies"] ||= {}
224
437
  base
225
438
  end
226
439
  end
@@ -249,6 +462,26 @@ module Aidp
249
462
  state["detection_comments"]
250
463
  end
251
464
 
465
+ def feedback_comments
466
+ state["feedback_comments"]
467
+ end
468
+
469
+ def processed_reactions
470
+ state["processed_reactions"]
471
+ end
472
+
473
+ def auto_prs
474
+ state["auto_prs"]
475
+ end
476
+
477
+ def projects
478
+ state["projects"]
479
+ end
480
+
481
+ def hierarchies
482
+ state["hierarchies"]
483
+ end
484
+
252
485
  def stringify_keys(hash)
253
486
  return {} unless hash
254
487