aidp 0.34.1 → 0.39.3

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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/aidp/agent_harness_adapter.rb +309 -0
  4. data/lib/aidp/cli/issue_importer.rb +20 -6
  5. data/lib/aidp/cli/jobs_command.rb +1 -1
  6. data/lib/aidp/cli/models_command.rb +9 -16
  7. data/lib/aidp/cli/prompts_command.rb +253 -0
  8. data/lib/aidp/cli/security_command.rb +4 -10
  9. data/lib/aidp/cli/storage_command.rb +277 -0
  10. data/lib/aidp/cli/temporal_command.rb +460 -0
  11. data/lib/aidp/cli.rb +24 -2
  12. data/lib/aidp/config.rb +15 -0
  13. data/lib/aidp/config_paths.rb +12 -2
  14. data/lib/aidp/database/migrations.rb +114 -0
  15. data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
  16. data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
  17. data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
  18. data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
  19. data/lib/aidp/database/repositories/job_repository.rb +278 -0
  20. data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
  21. data/lib/aidp/database/repositories/progress_repository.rb +210 -0
  22. data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
  23. data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
  24. data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
  25. data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
  26. data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
  27. data/lib/aidp/database/repositories/task_repository.rb +231 -0
  28. data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
  29. data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
  30. data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
  31. data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
  32. data/lib/aidp/database/repository.rb +166 -0
  33. data/lib/aidp/database/schema.rb +347 -0
  34. data/lib/aidp/database/storage_migrator.rb +693 -0
  35. data/lib/aidp/database.rb +139 -0
  36. data/lib/aidp/debug_mixin.rb +2 -2
  37. data/lib/aidp/execute/checkpoint.rb +26 -0
  38. data/lib/aidp/execute/checkpoint_display.rb +16 -0
  39. data/lib/aidp/execute/prompt_evaluator.rb +30 -57
  40. data/lib/aidp/execute/work_loop_runner.rb +294 -46
  41. data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
  42. data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
  43. data/lib/aidp/harness/ai_decision_engine.rb +78 -296
  44. data/lib/aidp/harness/config_schema.rb +59 -0
  45. data/lib/aidp/harness/configuration.rb +229 -10
  46. data/lib/aidp/harness/deprecation_cache.rb +53 -11
  47. data/lib/aidp/harness/enhanced_runner.rb +1 -1
  48. data/lib/aidp/harness/error_handler.rb +11 -5
  49. data/lib/aidp/harness/provider_config.rb +36 -0
  50. data/lib/aidp/harness/provider_factory.rb +26 -24
  51. data/lib/aidp/harness/provider_info.rb +9 -5
  52. data/lib/aidp/harness/provider_manager.rb +2 -2
  53. data/lib/aidp/harness/provider_metrics.rb +103 -1
  54. data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
  55. data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
  56. data/lib/aidp/harness/runner.rb +7 -2
  57. data/lib/aidp/harness/state_manager.rb +1 -1
  58. data/lib/aidp/harness/test_runner.rb +167 -11
  59. data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
  60. data/lib/aidp/harness/usage_limit.rb +179 -0
  61. data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
  62. data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
  63. data/lib/aidp/harness/usage_period.rb +234 -0
  64. data/lib/aidp/init/agent_instructions_generator.rb +317 -0
  65. data/lib/aidp/init/runner.rb +25 -4
  66. data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
  67. data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
  68. data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
  69. data/lib/aidp/interfaces/logger_interface.rb +150 -0
  70. data/lib/aidp/interfaces/ui_interface.rb +254 -0
  71. data/lib/aidp/logger.rb +25 -7
  72. data/lib/aidp/orchestration_adapter.rb +279 -0
  73. data/lib/aidp/pr_worktree_manager.rb +4 -15
  74. data/lib/aidp/prompts/feedback_collector.rb +198 -0
  75. data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
  76. data/lib/aidp/prompts/template_evolver.rb +305 -0
  77. data/lib/aidp/prompts/template_version_manager.rb +325 -0
  78. data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
  79. data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
  80. data/lib/aidp/setup/wizard.rb +616 -176
  81. data/lib/aidp/shell_executor.rb +58 -0
  82. data/lib/aidp/style_guide/selector.rb +56 -1
  83. data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
  84. data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
  85. data/lib/aidp/temporal/activities/base_activity.rb +78 -0
  86. data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
  87. data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
  88. data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
  89. data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
  90. data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
  91. data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
  92. data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
  93. data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
  94. data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
  95. data/lib/aidp/temporal/configuration.rb +169 -0
  96. data/lib/aidp/temporal/connection.rb +93 -0
  97. data/lib/aidp/temporal/worker.rb +119 -0
  98. data/lib/aidp/temporal/workflow_client.rb +171 -0
  99. data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
  100. data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
  101. data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
  102. data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
  103. data/lib/aidp/temporal.rb +120 -0
  104. data/lib/aidp/version.rb +1 -1
  105. data/lib/aidp/watch/base_processor.rb +35 -0
  106. data/lib/aidp/watch/build_processor.rb +132 -0
  107. data/lib/aidp/watch/github_state_extractor.rb +0 -8
  108. data/lib/aidp/watch/rebase_processor.rb +245 -0
  109. data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
  110. data/lib/aidp/watch/runner.rb +366 -285
  111. data/lib/aidp/watch/state_store.rb +80 -0
  112. data/lib/aidp/watch/work_item.rb +96 -0
  113. data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
  114. data/lib/aidp/watch/worktree_reconciler.rb +643 -0
  115. data/lib/aidp/workflows/guided_agent.rb +1 -1
  116. data/lib/aidp/worktree.rb +62 -0
  117. data/templates/aidp.yml.example +70 -0
  118. metadata +117 -16
  119. data/lib/aidp/analyze/json_file_storage.rb +0 -292
  120. data/lib/aidp/execute/future_work_backlog.rb +0 -411
  121. data/lib/aidp/providers/adapter.rb +0 -239
  122. data/lib/aidp/providers/aider.rb +0 -264
  123. data/lib/aidp/providers/anthropic.rb +0 -614
  124. data/lib/aidp/providers/base.rb +0 -617
  125. data/lib/aidp/providers/capability_registry.rb +0 -204
  126. data/lib/aidp/providers/codex.rb +0 -267
  127. data/lib/aidp/providers/cursor.rb +0 -304
  128. data/lib/aidp/providers/error_taxonomy.rb +0 -195
  129. data/lib/aidp/providers/gemini.rb +0 -110
  130. data/lib/aidp/providers/github_copilot.rb +0 -227
  131. data/lib/aidp/providers/kilocode.rb +0 -157
  132. data/lib/aidp/providers/opencode.rb +0 -139
@@ -3,6 +3,10 @@
3
3
  require "tty-prompt"
4
4
  require_relative "feedback_collector"
5
5
  require_relative "github_state_extractor"
6
+ require_relative "round_robin_scheduler"
7
+ require_relative "work_item"
8
+ require_relative "worktree_cleanup_job"
9
+ require_relative "worktree_reconciler"
6
10
 
7
11
  module Aidp
8
12
  module Watch
@@ -115,6 +119,30 @@ module Aidp
115
119
  state_store: @state_store,
116
120
  project_dir: project_dir
117
121
  )
122
+
123
+ # Initialize worktree cleanup job (issue #367)
124
+ cleanup_config = Aidp::Config.worktree_cleanup_config(project_dir)
125
+ @worktree_cleanup_job = WorktreeCleanupJob.new(
126
+ project_dir: project_dir,
127
+ config: cleanup_config
128
+ )
129
+
130
+ # Initialize worktree reconciler for dirty worktree handling
131
+ reconciler_config = safety_config[:worktree_reconciliation] || safety_config["worktree_reconciliation"] || {}
132
+ @worktree_reconciler = WorktreeReconciler.new(
133
+ project_dir: project_dir,
134
+ repository_client: @repository_client,
135
+ build_processor: @build_processor,
136
+ state_store: @state_store,
137
+ config: reconciler_config
138
+ )
139
+ @last_reconcile_at = nil
140
+
141
+ # Initialize round-robin scheduler (issue #434)
142
+ @round_robin_scheduler = RoundRobinScheduler.new(
143
+ state_store: @state_store
144
+ )
145
+ @needs_input_label = label_config[:needs_input] || label_config["needs_input"] || "aidp-needs-input"
118
146
  end
119
147
 
120
148
  def start
@@ -156,343 +184,342 @@ module Aidp
156
184
  private
157
185
 
158
186
  def process_cycle
159
- process_plan_triggers
160
- process_build_triggers
161
- process_auto_issue_triggers
187
+ # Collect all work items and refresh the scheduler queue
188
+ work_items = collect_all_work_items
189
+ @round_robin_scheduler.refresh_queue(work_items)
190
+
191
+ # Get paused items (aidp-needs-input label)
192
+ paused_numbers = fetch_paused_item_numbers
193
+
194
+ # Log queue status
195
+ stats = @round_robin_scheduler.stats
196
+ Aidp.log_debug("watch_runner", "round_robin.queue_status",
197
+ total: stats[:total],
198
+ by_processor: stats[:by_processor],
199
+ paused_count: paused_numbers.size)
200
+
201
+ # Process one work item using round-robin
202
+ if @round_robin_scheduler.work?(paused_numbers: paused_numbers)
203
+ process_next_work_item(paused_numbers: paused_numbers)
204
+ else
205
+ Aidp.log_debug("watch_runner", "round_robin.no_work",
206
+ queue_size: stats[:total], paused_count: paused_numbers.size)
207
+ end
208
+
209
+ # Run maintenance tasks (these run every cycle regardless)
162
210
  check_for_updates_if_due
163
- process_review_triggers
164
- process_ci_fix_triggers
165
- process_auto_pr_triggers
166
- process_change_request_triggers
167
211
  collect_feedback
212
+ process_worktree_cleanup
213
+ process_worktree_reconciliation
168
214
  end
169
215
 
170
- def process_plan_triggers
171
- plan_label = @plan_processor.plan_label
172
- begin
173
- issues = @repository_client.list_issues(labels: [plan_label], state: "open")
174
- rescue => e
175
- Aidp.log_error("watch_runner", "plan_poll_failed", label: plan_label, error: e.message)
176
- return # Skip this cycle, continue watch loop
177
- end
178
- Aidp.log_debug("watch_runner", "plan_poll", label: plan_label, total: issues.size)
179
- issues.each do |issue|
180
- unless issue_has_label?(issue, plan_label)
181
- Aidp.log_debug("watch_runner", "plan_skip_label_mismatch", issue: issue[:number], labels: issue[:labels])
182
- next
183
- end
216
+ # Process the next work item from the round-robin queue.
217
+ # @param paused_numbers [Array<Integer>] Numbers of paused items to skip
218
+ def process_next_work_item(paused_numbers:)
219
+ item = @round_robin_scheduler.next_item(paused_numbers: paused_numbers)
220
+ return unless item
184
221
 
185
- begin
186
- detailed = @repository_client.fetch_issue(issue[:number])
187
- rescue => e
188
- Aidp.log_error("watch_runner", "fetch_issue_failed", issue: issue[:number], error: e.message)
189
- next # Skip this issue, continue with others
190
- end
222
+ Aidp.log_info("watch_runner", "round_robin.processing",
223
+ key: item.key, number: item.number, processor_type: item.processor_type)
191
224
 
192
- # Check if already in progress by another instance
193
- if @state_extractor.in_progress?(detailed)
194
- Aidp.log_debug("watch_runner", "plan_skip_in_progress", issue: detailed[:number])
195
- next
196
- end
225
+ # Dispatch to appropriate processor - returns true if item was actually processed
226
+ was_processed = dispatch_work_item(item)
197
227
 
198
- # Check author authorization before processing
199
- unless @safety_checker.should_process_issue?(detailed, enforce: false)
200
- Aidp.log_debug("watch_runner", "plan_skip_unauthorized_author", issue: detailed[:number],
201
- author: detailed[:author])
202
- next
203
- end
228
+ # Only mark as processed if work was actually done (not skipped), so skipped
229
+ # items remain in the queue and will be re-checked on the next cycle.
230
+ @round_robin_scheduler.mark_processed(item) if was_processed
231
+ rescue => e
232
+ Aidp.log_error("watch_runner", "round_robin.process_failed",
233
+ key: item&.key, error: e.message, error_class: e.class.name)
234
+ # Mark as processed on error to prevent infinite retry loops within the same
235
+ # rotation. Unlike authorization/build_completed skips (which return false to
236
+ # allow immediate retry since their state may change), errors are assumed to be
237
+ # transient issues that will resolve by the next full queue refresh cycle.
238
+ # The item will be re-added to the queue on the next refresh if still active.
239
+ @round_robin_scheduler.mark_processed(item) if item
240
+ end
204
241
 
205
- # Check if detection comment already posted (deduplication)
206
- unless @state_extractor.detection_comment_posted?(detailed, plan_label)
207
- post_detection_comment(item_type: :issue, number: detailed[:number], label: plan_label)
208
- end
242
+ # Dispatch a work item to its appropriate processor.
243
+ #
244
+ # Design note: Items skipped due to authorization or build_completed checks
245
+ # return false and are NOT marked as processed. This means they will be
246
+ # re-checked on the next cycle. This is intentional because:
247
+ # - Authorization: An author could be added to the allowlist between cycles
248
+ # - Build completed: New commits could be pushed, requiring more work
249
+ #
250
+ # The trade-off is some redundant API calls, but this ensures we don't
251
+ # permanently skip items whose state could change.
252
+ #
253
+ # @param item [WorkItem] The work item to process
254
+ # @return [Boolean] True if item was actually processed, false if skipped
255
+ def dispatch_work_item(item)
256
+ detailed = fetch_detailed_item(item)
257
+ return false unless detailed
258
+
259
+ # Check authorization
260
+ unless @safety_checker.should_process_issue?(detailed, enforce: false)
261
+ Aidp.log_debug("watch_runner", "round_robin.skip_unauthorized",
262
+ key: item.key, author: detailed[:author])
263
+ return false
264
+ end
209
265
 
210
- Aidp.log_debug("watch_runner", "plan_process", issue: detailed[:number])
266
+ # Post detection comment if not already posted
267
+ post_detection_comment_for_item(item, detailed)
268
+
269
+ # Dispatch to processor
270
+ case item.processor_type
271
+ when :plan
211
272
  @plan_processor.process(detailed)
212
- rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
213
- Aidp.log_warn("watch_runner", "unauthorized issue author", issue: issue[:number], error: e.message)
273
+ when :build
274
+ # Check build completion at dispatch time (moved from collection for API efficiency)
275
+ if @state_extractor.build_completed?(detailed)
276
+ Aidp.log_debug("watch_runner", "round_robin.skip_build_completed",
277
+ key: item.key, number: item.number)
278
+ return false
279
+ end
280
+ @build_processor.process(detailed)
281
+ when :auto_issue
282
+ @auto_processor.process(detailed)
283
+ when :review
284
+ @review_processor.process(detailed)
285
+ when :ci_fix
286
+ @ci_fix_processor.process(detailed)
287
+ when :auto_pr
288
+ @auto_pr_processor.process(detailed)
289
+ when :change_request
290
+ @change_request_processor.process(detailed)
291
+ else
292
+ Aidp.log_warn("watch_runner", "round_robin.unknown_processor",
293
+ processor_type: item.processor_type)
294
+ return false
214
295
  end
296
+
297
+ true # Item was processed
298
+ rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
299
+ Aidp.log_warn("watch_runner", "round_robin.unauthorized_author",
300
+ key: item.key, error: e.message)
301
+ false
215
302
  end
216
303
 
217
- def process_build_triggers
218
- build_label = @build_processor.build_label
219
- begin
220
- issues = @repository_client.list_issues(labels: [build_label], state: "open")
221
- rescue => e
222
- Aidp.log_error("watch_runner", "build_poll_failed", label: build_label, error: e.message)
223
- return # Skip this cycle, continue watch loop
224
- end
225
- Aidp.log_debug("watch_runner", "build_poll", label: build_label, total: issues.size)
226
- issues.each do |issue|
227
- detailed = nil
228
- in_progress_added = false
229
-
230
- begin
231
- unless issue_has_label?(issue, build_label)
232
- Aidp.log_debug("watch_runner", "build_skip_label_mismatch", issue: issue[:number], labels: issue[:labels])
233
- next
234
- end
235
-
236
- begin
237
- detailed = @repository_client.fetch_issue(issue[:number])
238
- rescue => e
239
- Aidp.log_error("watch_runner", "fetch_issue_failed", issue: issue[:number], error: e.message)
240
- next # Skip this issue, continue with others
241
- end
242
-
243
- # Check if already completed (via GitHub comments)
244
- if @state_extractor.build_completed?(detailed)
245
- Aidp.log_debug("watch_runner", "build_skip_completed", issue: detailed[:number])
246
- next
247
- end
248
-
249
- # Check if already in progress by another instance
250
- if @state_extractor.in_progress?(detailed)
251
- Aidp.log_debug("watch_runner", "build_skip_in_progress", issue: detailed[:number])
252
- next
253
- end
254
-
255
- # Check author authorization before processing
256
- unless @safety_checker.should_process_issue?(detailed, enforce: false)
257
- Aidp.log_debug("watch_runner", "build_skip_unauthorized_author", issue: detailed[:number],
258
- author: detailed[:author])
259
- next
260
- end
261
-
262
- # Check if detection comment already posted (deduplication)
263
- unless @state_extractor.detection_comment_posted?(detailed, build_label)
264
- post_detection_comment(item_type: :issue, number: detailed[:number], label: build_label)
265
- end
266
-
267
- # Add in-progress label before processing
268
- @repository_client.add_labels(detailed[:number], GitHubStateExtractor::IN_PROGRESS_LABEL)
269
- in_progress_added = true
270
-
271
- Aidp.log_debug("watch_runner", "build_process", issue: detailed[:number])
272
- @build_processor.process(detailed)
273
- rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
274
- Aidp.log_warn("watch_runner", "unauthorized issue author", issue: issue[:number], error: e.message)
275
- ensure
276
- # Remove in-progress label when done (only if we added it)
277
- if in_progress_added && detailed
278
- begin
279
- @repository_client.remove_labels(detailed[:number], GitHubStateExtractor::IN_PROGRESS_LABEL)
280
- rescue => e
281
- Aidp.log_warn("watch_runner", "failed_to_remove_in_progress_label", issue: detailed[:number],
282
- error: e.message)
283
- end
284
- end
285
- end
304
+ # Fetch detailed issue or PR data for a work item.
305
+ # @param item [WorkItem] Work item to fetch details for
306
+ # @return [Hash, nil] Detailed item data or nil on error
307
+ def fetch_detailed_item(item)
308
+ if item.issue?
309
+ @repository_client.fetch_issue(item.number)
310
+ else
311
+ @repository_client.fetch_pull_request(item.number)
286
312
  end
313
+ rescue => e
314
+ Aidp.log_error("watch_runner", "round_robin.fetch_failed",
315
+ key: item.key, error: e.message)
316
+ nil
287
317
  end
288
318
 
289
- def process_auto_issue_triggers
290
- auto_label = @auto_processor.auto_label
291
- begin
292
- issues = @repository_client.list_issues(labels: [auto_label], state: "open")
293
- rescue => e
294
- Aidp.log_error("watch_runner", "auto_issue_poll_failed", label: auto_label, error: e.message)
295
- return
296
- end
319
+ # Post detection comment for a work item if not already posted.
320
+ # @param item [WorkItem] Work item
321
+ # @param detailed [Hash] Detailed issue/PR data
322
+ def post_detection_comment_for_item(item, detailed)
323
+ return unless @post_detection_comments
324
+ return if @state_extractor.detection_comment_posted?(detailed, item.label)
297
325
 
298
- Aidp.log_debug("watch_runner", "auto_issue_poll", label: auto_label, total: issues.size)
326
+ post_detection_comment(
327
+ item_type: item.item_type,
328
+ number: item.number,
329
+ label: item.label
330
+ )
331
+ end
299
332
 
300
- issues.each do |issue|
301
- unless issue_has_label?(issue, auto_label)
302
- Aidp.log_debug("watch_runner", "auto_issue_skip_label_mismatch", issue: issue[:number],
303
- labels: issue[:labels])
304
- next
305
- end
333
+ # Collect all work items from all processors.
334
+ # @return [Array<WorkItem>] List of all work items
335
+ def collect_all_work_items
336
+ items = []
306
337
 
307
- begin
308
- detailed = @repository_client.fetch_issue(issue[:number])
309
- rescue => e
310
- Aidp.log_error("watch_runner", "auto_issue_fetch_failed", issue: issue[:number], error: e.message)
311
- next
312
- end
338
+ items.concat(collect_plan_work_items)
339
+ items.concat(collect_build_work_items)
340
+ items.concat(collect_auto_issue_work_items)
341
+ items.concat(collect_review_work_items)
342
+ items.concat(collect_ci_fix_work_items)
343
+ items.concat(collect_auto_pr_work_items)
344
+ items.concat(collect_change_request_work_items)
313
345
 
314
- # Check if already in progress by another instance
315
- if @state_extractor.in_progress?(detailed)
316
- Aidp.log_debug("watch_runner", "auto_issue_skip_in_progress", issue: detailed[:number])
317
- next
318
- end
346
+ Aidp.log_debug("watch_runner", "collect_all_work_items.complete",
347
+ total: items.size)
319
348
 
320
- # Check author authorization before processing
321
- unless @safety_checker.should_process_issue?(detailed, enforce: false)
322
- Aidp.log_debug("watch_runner", "auto_issue_skip_unauthorized_author", issue: detailed[:number],
323
- author: detailed[:author])
324
- next
325
- end
349
+ items
350
+ end
326
351
 
327
- # Check if detection comment already posted (deduplication)
328
- unless @state_extractor.detection_comment_posted?(detailed, auto_label)
329
- post_detection_comment(item_type: :issue, number: detailed[:number], label: auto_label)
330
- end
352
+ # Collect work items for plan triggers.
353
+ # @return [Array<WorkItem>]
354
+ def collect_plan_work_items
355
+ label = @plan_processor.plan_label
356
+ issues = @repository_client.list_issues(labels: [label], state: "open")
331
357
 
332
- Aidp.log_debug("watch_runner", "auto_issue_process", issue: detailed[:number])
333
- @auto_processor.process(detailed)
334
- rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
335
- Aidp.log_warn("watch_runner", "unauthorized_issue_author_auto", issue: issue[:number], error: e.message)
358
+ issues.filter_map do |issue|
359
+ next unless issue_has_label?(issue, label)
360
+
361
+ WorkItem.new(
362
+ number: issue[:number],
363
+ item_type: :issue,
364
+ processor_type: :plan,
365
+ label: label,
366
+ data: issue
367
+ )
336
368
  end
369
+ rescue => e
370
+ Aidp.log_error("watch_runner", "collect_plan_items_failed", error: e.message)
371
+ []
337
372
  end
338
373
 
339
- def process_review_triggers
340
- review_label = @review_processor.review_label
341
- begin
342
- prs = @repository_client.list_pull_requests(labels: [review_label], state: "open")
343
- rescue => e
344
- Aidp.log_error("watch_runner", "review_poll_failed", label: review_label, error: e.message)
345
- return # Skip this cycle, continue watch loop
374
+ # Collect work items for build triggers.
375
+ # @return [Array<WorkItem>]
376
+ def collect_build_work_items
377
+ label = @build_processor.build_label
378
+ issues = @repository_client.list_issues(labels: [label], state: "open")
379
+
380
+ issues.filter_map do |issue|
381
+ next unless issue_has_label?(issue, label)
382
+
383
+ # Note: build_completed check moved to dispatch phase to avoid
384
+ # API calls during collection (addresses rate limiting concerns)
385
+ WorkItem.new(
386
+ number: issue[:number],
387
+ item_type: :issue,
388
+ processor_type: :build,
389
+ label: label,
390
+ data: issue
391
+ )
346
392
  end
347
- Aidp.log_debug("watch_runner", "review_poll", label: review_label, total: prs.size)
348
- prs.each do |pr|
349
- unless pr_has_label?(pr, review_label)
350
- Aidp.log_debug("watch_runner", "review_skip_label_mismatch", pr: pr[:number], labels: pr[:labels])
351
- next
352
- end
353
-
354
- begin
355
- detailed = @repository_client.fetch_pull_request(pr[:number])
356
- rescue => e
357
- Aidp.log_error("watch_runner", "fetch_pr_failed", pr: pr[:number], error: e.message)
358
- next # Skip this PR, continue with others
359
- end
393
+ rescue => e
394
+ Aidp.log_error("watch_runner", "collect_build_items_failed", error: e.message)
395
+ []
396
+ end
360
397
 
361
- # Check if already in progress by another instance
362
- if @state_extractor.in_progress?(detailed)
363
- Aidp.log_debug("watch_runner", "review_skip_in_progress", pr: detailed[:number])
364
- next
365
- end
398
+ # Collect work items for auto issue triggers.
399
+ # @return [Array<WorkItem>]
400
+ def collect_auto_issue_work_items
401
+ label = @auto_processor.auto_label
402
+ issues = @repository_client.list_issues(labels: [label], state: "open")
366
403
 
367
- # Check author authorization before processing
368
- unless @safety_checker.should_process_issue?(detailed, enforce: false)
369
- Aidp.log_debug("watch_runner", "review_skip_unauthorized_author", pr: detailed[:number],
370
- author: detailed[:author])
371
- next
372
- end
404
+ issues.filter_map do |issue|
405
+ next unless issue_has_label?(issue, label)
373
406
 
374
- # Check if detection comment already posted (deduplication)
375
- unless @state_extractor.detection_comment_posted?(detailed, review_label)
376
- post_detection_comment(item_type: :pr, number: detailed[:number], label: review_label)
377
- end
378
-
379
- Aidp.log_debug("watch_runner", "review_process", pr: detailed[:number])
380
- @review_processor.process(detailed)
381
- rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
382
- Aidp.log_warn("watch_runner", "unauthorized PR author", pr: pr[:number], error: e.message)
407
+ WorkItem.new(
408
+ number: issue[:number],
409
+ item_type: :issue,
410
+ processor_type: :auto_issue,
411
+ label: label,
412
+ data: issue
413
+ )
383
414
  end
415
+ rescue => e
416
+ Aidp.log_error("watch_runner", "collect_auto_issue_items_failed", error: e.message)
417
+ []
384
418
  end
385
419
 
386
- def process_auto_pr_triggers
387
- auto_label = @auto_pr_processor.auto_label
388
- prs = @repository_client.list_pull_requests(labels: [auto_label], state: "open")
389
- Aidp.log_debug("watch_runner", "auto_pr_poll", label: auto_label, total: prs.size)
420
+ # Collect work items for review triggers.
421
+ # @return [Array<WorkItem>]
422
+ def collect_review_work_items
423
+ label = @review_processor.review_label
424
+ prs = @repository_client.list_pull_requests(labels: [label], state: "open")
390
425
 
391
- prs.each do |pr|
392
- unless pr_has_label?(pr, auto_label)
393
- Aidp.log_debug("watch_runner", "auto_pr_skip_label_mismatch", pr: pr[:number], labels: pr[:labels])
394
- next
395
- end
396
-
397
- detailed = @repository_client.fetch_pull_request(pr[:number])
426
+ prs.filter_map do |pr|
427
+ next unless pr_has_label?(pr, label)
398
428
 
399
- # Check if already in progress by another instance
400
- if @state_extractor.in_progress?(detailed)
401
- Aidp.log_debug("watch_runner", "auto_pr_skip_in_progress", pr: detailed[:number])
402
- next
403
- end
429
+ WorkItem.new(
430
+ number: pr[:number],
431
+ item_type: :pr,
432
+ processor_type: :review,
433
+ label: label,
434
+ data: pr
435
+ )
436
+ end
437
+ rescue => e
438
+ Aidp.log_error("watch_runner", "collect_review_items_failed", error: e.message)
439
+ []
440
+ end
404
441
 
405
- # Check author authorization before processing
406
- unless @safety_checker.should_process_issue?(detailed, enforce: false)
407
- Aidp.log_debug("watch_runner", "auto_pr_skip_unauthorized_author", pr: detailed[:number],
408
- author: detailed[:author])
409
- next
410
- end
442
+ # Collect work items for CI fix triggers.
443
+ # @return [Array<WorkItem>]
444
+ def collect_ci_fix_work_items
445
+ label = @ci_fix_processor.ci_fix_label
446
+ prs = @repository_client.list_pull_requests(labels: [label], state: "open")
411
447
 
412
- # Check if detection comment already posted (deduplication)
413
- unless @state_extractor.detection_comment_posted?(detailed, auto_label)
414
- post_detection_comment(item_type: :pr, number: detailed[:number], label: auto_label)
415
- end
448
+ prs.filter_map do |pr|
449
+ next unless pr_has_label?(pr, label)
416
450
 
417
- Aidp.log_debug("watch_runner", "auto_pr_process", pr: detailed[:number])
418
- @auto_pr_processor.process(detailed)
419
- rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
420
- Aidp.log_warn("watch_runner", "unauthorized_pr_author_auto", pr: pr[:number], error: e.message)
451
+ WorkItem.new(
452
+ number: pr[:number],
453
+ item_type: :pr,
454
+ processor_type: :ci_fix,
455
+ label: label,
456
+ data: pr
457
+ )
421
458
  end
459
+ rescue => e
460
+ Aidp.log_error("watch_runner", "collect_ci_fix_items_failed", error: e.message)
461
+ []
422
462
  end
423
463
 
424
- def process_ci_fix_triggers
425
- ci_fix_label = @ci_fix_processor.ci_fix_label
426
- prs = @repository_client.list_pull_requests(labels: [ci_fix_label], state: "open")
427
- Aidp.log_debug("watch_runner", "ci_fix_poll", label: ci_fix_label, total: prs.size)
428
- prs.each do |pr|
429
- unless pr_has_label?(pr, ci_fix_label)
430
- Aidp.log_debug("watch_runner", "ci_fix_skip_label_mismatch", pr: pr[:number], labels: pr[:labels])
431
- next
432
- end
464
+ # Collect work items for auto PR triggers.
465
+ # @return [Array<WorkItem>]
466
+ def collect_auto_pr_work_items
467
+ label = @auto_pr_processor.auto_label
468
+ prs = @repository_client.list_pull_requests(labels: [label], state: "open")
433
469
 
434
- detailed = @repository_client.fetch_pull_request(pr[:number])
470
+ prs.filter_map do |pr|
471
+ next unless pr_has_label?(pr, label)
435
472
 
436
- # Check if already in progress by another instance
437
- if @state_extractor.in_progress?(detailed)
438
- Aidp.log_debug("watch_runner", "ci_fix_skip_in_progress", pr: detailed[:number])
439
- next
440
- end
473
+ WorkItem.new(
474
+ number: pr[:number],
475
+ item_type: :pr,
476
+ processor_type: :auto_pr,
477
+ label: label,
478
+ data: pr
479
+ )
480
+ end
481
+ rescue => e
482
+ Aidp.log_error("watch_runner", "collect_auto_pr_items_failed", error: e.message)
483
+ []
484
+ end
441
485
 
442
- # Check author authorization before processing
443
- unless @safety_checker.should_process_issue?(detailed, enforce: false)
444
- Aidp.log_debug("watch_runner", "ci_fix_skip_unauthorized_author", pr: detailed[:number],
445
- author: detailed[:author])
446
- next
447
- end
486
+ # Collect work items for change request triggers.
487
+ # @return [Array<WorkItem>]
488
+ def collect_change_request_work_items
489
+ label = @change_request_processor.change_request_label
490
+ prs = @repository_client.list_pull_requests(labels: [label], state: "open")
448
491
 
449
- # Check if detection comment already posted (deduplication)
450
- unless @state_extractor.detection_comment_posted?(detailed, ci_fix_label)
451
- post_detection_comment(item_type: :pr, number: detailed[:number], label: ci_fix_label)
452
- end
492
+ prs.filter_map do |pr|
493
+ next unless pr_has_label?(pr, label)
453
494
 
454
- Aidp.log_debug("watch_runner", "ci_fix_process", pr: detailed[:number])
455
- @ci_fix_processor.process(detailed)
456
- rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
457
- Aidp.log_warn("watch_runner", "unauthorized PR author", pr: pr[:number], error: e.message)
495
+ WorkItem.new(
496
+ number: pr[:number],
497
+ item_type: :pr,
498
+ processor_type: :change_request,
499
+ label: label,
500
+ data: pr
501
+ )
458
502
  end
503
+ rescue => e
504
+ Aidp.log_error("watch_runner", "collect_change_request_items_failed", error: e.message)
505
+ []
459
506
  end
460
507
 
461
- def process_change_request_triggers
462
- change_request_label = @change_request_processor.change_request_label
463
- prs = @repository_client.list_pull_requests(labels: [change_request_label], state: "open")
464
- Aidp.log_debug("watch_runner", "change_request_poll", label: change_request_label, total: prs.size)
465
- prs.each do |pr|
466
- unless pr_has_label?(pr, change_request_label)
467
- Aidp.log_debug("watch_runner", "change_request_skip_label_mismatch", pr: pr[:number], labels: pr[:labels])
468
- next
469
- end
508
+ # Fetch the numbers of all paused items (with aidp-needs-input label).
509
+ # @return [Array<Integer>] Issue/PR numbers that are paused
510
+ def fetch_paused_item_numbers
511
+ issues = @repository_client.list_issues(labels: [@needs_input_label], state: "open")
512
+ prs = @repository_client.list_pull_requests(labels: [@needs_input_label], state: "open")
470
513
 
471
- detailed = @repository_client.fetch_pull_request(pr[:number])
514
+ paused = issues.map { |i| i[:number] } + prs.map { |p| p[:number] }
472
515
 
473
- # Check if already in progress by another instance
474
- if @state_extractor.in_progress?(detailed)
475
- Aidp.log_debug("watch_runner", "change_request_skip_in_progress", pr: detailed[:number])
476
- next
477
- end
516
+ Aidp.log_debug("watch_runner", "fetch_paused_items",
517
+ count: paused.size, numbers: paused)
478
518
 
479
- # Check author authorization before processing
480
- unless @safety_checker.should_process_issue?(detailed, enforce: false)
481
- Aidp.log_debug("watch_runner", "change_request_skip_unauthorized_author", pr: detailed[:number],
482
- author: detailed[:author])
483
- next
484
- end
485
-
486
- # Check if detection comment already posted (deduplication)
487
- unless @state_extractor.detection_comment_posted?(detailed, change_request_label)
488
- post_detection_comment(item_type: :pr, number: detailed[:number], label: change_request_label)
489
- end
490
-
491
- Aidp.log_debug("watch_runner", "change_request_process", pr: detailed[:number])
492
- @change_request_processor.process(detailed)
493
- rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
494
- Aidp.log_warn("watch_runner", "unauthorized PR author", pr: pr[:number], error: e.message)
495
- end
519
+ paused
520
+ rescue => e
521
+ Aidp.log_error("watch_runner", "fetch_paused_items_failed", error: e.message)
522
+ []
496
523
  end
497
524
 
498
525
  def issue_has_label?(issue, label)
@@ -684,6 +711,60 @@ module Aidp
684
711
  Aidp.log_error("watch_runner", "feedback_collection_failed", error: e.message)
685
712
  display_message("⚠️ Feedback collection failed: #{e.message}", type: :warn) if @verbose
686
713
  end
714
+
715
+ # Process worktree cleanup if due (issue #367)
716
+ # Runs periodically based on configured frequency (daily/weekly)
717
+ def process_worktree_cleanup
718
+ return unless @worktree_cleanup_job.enabled?
719
+
720
+ last_cleanup = @state_store.last_worktree_cleanup
721
+ return unless @worktree_cleanup_job.cleanup_due?(last_cleanup)
722
+
723
+ Aidp.log_debug("watch_runner", "worktree_cleanup.checking",
724
+ last_cleanup: last_cleanup&.iso8601,
725
+ frequency: @worktree_cleanup_job.cleanup_interval_seconds)
726
+
727
+ result = @worktree_cleanup_job.execute
728
+
729
+ @state_store.record_worktree_cleanup(
730
+ cleaned: result[:cleaned],
731
+ skipped: result[:skipped],
732
+ errors: result[:errors]
733
+ )
734
+
735
+ if result[:cleaned] > 0 || @verbose
736
+ display_message("🧹 Worktree cleanup: #{result[:cleaned]} cleaned, #{result[:skipped]} skipped",
737
+ type: :info)
738
+ end
739
+ rescue => e
740
+ Aidp.log_error("watch_runner", "worktree_cleanup_failed", error: e.message)
741
+ display_message("⚠️ Worktree cleanup failed: #{e.message}", type: :warn) if @verbose
742
+ end
743
+
744
+ # Process worktree reconciliation for dirty worktrees
745
+ # Resumes interrupted work or reconciles merged PRs
746
+ def process_worktree_reconciliation
747
+ return unless @worktree_reconciler.enabled?
748
+ return unless @worktree_reconciler.reconciliation_due?(@last_reconcile_at)
749
+
750
+ Aidp.log_debug("watch_runner", "worktree_reconciliation.checking",
751
+ last_reconcile: @last_reconcile_at&.iso8601)
752
+
753
+ result = @worktree_reconciler.execute
754
+ @last_reconcile_at = Time.now
755
+
756
+ total_actions = result[:resumed] + result[:reconciled] + result[:cleaned]
757
+ if total_actions > 0 || @verbose
758
+ display_message(
759
+ "🔄 Worktree reconciliation: #{result[:resumed]} resumed, " \
760
+ "#{result[:reconciled]} reconciled, #{result[:cleaned]} cleaned",
761
+ type: :info
762
+ )
763
+ end
764
+ rescue => e
765
+ Aidp.log_error("watch_runner", "worktree_reconciliation_failed", error: e.message)
766
+ display_message("⚠️ Worktree reconciliation failed: #{e.message}", type: :warn) if @verbose
767
+ end
687
768
  end
688
769
  end
689
770
  end