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.
- checksums.yaml +4 -4
- data/README.md +14 -14
- data/lib/aidp/agent_harness_adapter.rb +309 -0
- data/lib/aidp/cli/issue_importer.rb +20 -6
- data/lib/aidp/cli/jobs_command.rb +1 -1
- data/lib/aidp/cli/models_command.rb +9 -16
- data/lib/aidp/cli/prompts_command.rb +253 -0
- data/lib/aidp/cli/security_command.rb +4 -10
- data/lib/aidp/cli/storage_command.rb +277 -0
- data/lib/aidp/cli/temporal_command.rb +460 -0
- data/lib/aidp/cli.rb +24 -2
- data/lib/aidp/config.rb +15 -0
- data/lib/aidp/config_paths.rb +12 -2
- data/lib/aidp/database/migrations.rb +114 -0
- data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
- data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
- data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
- data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
- data/lib/aidp/database/repositories/job_repository.rb +278 -0
- data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
- data/lib/aidp/database/repositories/progress_repository.rb +210 -0
- data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
- data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
- data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
- data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
- data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
- data/lib/aidp/database/repositories/task_repository.rb +231 -0
- data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
- data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
- data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
- data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
- data/lib/aidp/database/repository.rb +166 -0
- data/lib/aidp/database/schema.rb +347 -0
- data/lib/aidp/database/storage_migrator.rb +693 -0
- data/lib/aidp/database.rb +139 -0
- data/lib/aidp/debug_mixin.rb +2 -2
- data/lib/aidp/execute/checkpoint.rb +26 -0
- data/lib/aidp/execute/checkpoint_display.rb +16 -0
- data/lib/aidp/execute/prompt_evaluator.rb +30 -57
- data/lib/aidp/execute/work_loop_runner.rb +294 -46
- data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
- data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
- data/lib/aidp/harness/ai_decision_engine.rb +78 -296
- data/lib/aidp/harness/config_schema.rb +59 -0
- data/lib/aidp/harness/configuration.rb +229 -10
- data/lib/aidp/harness/deprecation_cache.rb +53 -11
- data/lib/aidp/harness/enhanced_runner.rb +1 -1
- data/lib/aidp/harness/error_handler.rb +11 -5
- data/lib/aidp/harness/provider_config.rb +36 -0
- data/lib/aidp/harness/provider_factory.rb +26 -24
- data/lib/aidp/harness/provider_info.rb +9 -5
- data/lib/aidp/harness/provider_manager.rb +2 -2
- data/lib/aidp/harness/provider_metrics.rb +103 -1
- data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
- data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
- data/lib/aidp/harness/runner.rb +7 -2
- data/lib/aidp/harness/state_manager.rb +1 -1
- data/lib/aidp/harness/test_runner.rb +167 -11
- data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
- data/lib/aidp/harness/usage_limit.rb +179 -0
- data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
- data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
- data/lib/aidp/harness/usage_period.rb +234 -0
- data/lib/aidp/init/agent_instructions_generator.rb +317 -0
- data/lib/aidp/init/runner.rb +25 -4
- data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
- data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
- data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
- data/lib/aidp/interfaces/logger_interface.rb +150 -0
- data/lib/aidp/interfaces/ui_interface.rb +254 -0
- data/lib/aidp/logger.rb +25 -7
- data/lib/aidp/orchestration_adapter.rb +279 -0
- data/lib/aidp/pr_worktree_manager.rb +4 -15
- data/lib/aidp/prompts/feedback_collector.rb +198 -0
- data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
- data/lib/aidp/prompts/template_evolver.rb +305 -0
- data/lib/aidp/prompts/template_version_manager.rb +325 -0
- data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
- data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
- data/lib/aidp/setup/wizard.rb +616 -176
- data/lib/aidp/shell_executor.rb +58 -0
- data/lib/aidp/style_guide/selector.rb +56 -1
- data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
- data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
- data/lib/aidp/temporal/activities/base_activity.rb +78 -0
- data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
- data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
- data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
- data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
- data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
- data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
- data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
- data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
- data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
- data/lib/aidp/temporal/configuration.rb +169 -0
- data/lib/aidp/temporal/connection.rb +93 -0
- data/lib/aidp/temporal/worker.rb +119 -0
- data/lib/aidp/temporal/workflow_client.rb +171 -0
- data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
- data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
- data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
- data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
- data/lib/aidp/temporal.rb +120 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/base_processor.rb +35 -0
- data/lib/aidp/watch/build_processor.rb +132 -0
- data/lib/aidp/watch/github_state_extractor.rb +0 -8
- data/lib/aidp/watch/rebase_processor.rb +245 -0
- data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
- data/lib/aidp/watch/runner.rb +366 -285
- data/lib/aidp/watch/state_store.rb +80 -0
- data/lib/aidp/watch/work_item.rb +96 -0
- data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
- data/lib/aidp/watch/worktree_reconciler.rb +643 -0
- data/lib/aidp/workflows/guided_agent.rb +1 -1
- data/lib/aidp/worktree.rb +62 -0
- data/templates/aidp.yml.example +70 -0
- metadata +117 -16
- data/lib/aidp/analyze/json_file_storage.rb +0 -292
- data/lib/aidp/execute/future_work_backlog.rb +0 -411
- data/lib/aidp/providers/adapter.rb +0 -239
- data/lib/aidp/providers/aider.rb +0 -264
- data/lib/aidp/providers/anthropic.rb +0 -614
- data/lib/aidp/providers/base.rb +0 -617
- data/lib/aidp/providers/capability_registry.rb +0 -204
- data/lib/aidp/providers/codex.rb +0 -267
- data/lib/aidp/providers/cursor.rb +0 -304
- data/lib/aidp/providers/error_taxonomy.rb +0 -195
- data/lib/aidp/providers/gemini.rb +0 -110
- data/lib/aidp/providers/github_copilot.rb +0 -227
- data/lib/aidp/providers/kilocode.rb +0 -157
- data/lib/aidp/providers/opencode.rb +0 -139
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
require "aidp/watch/base_processor"
|
|
2
|
+
require "aidp/worktree"
|
|
3
|
+
require "aidp/pr_worktree_manager"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Watch
|
|
7
|
+
# Processor for handling 'aidp-rebase' label on GitHub PRs
|
|
8
|
+
# Automatically rebases the PR branch against its target branch
|
|
9
|
+
class RebaseProcessor < BaseProcessor
|
|
10
|
+
DEFAULT_REBASE_LABEL = "aidp-rebase".freeze
|
|
11
|
+
|
|
12
|
+
def initialize(
|
|
13
|
+
repository_client:,
|
|
14
|
+
state_store: nil,
|
|
15
|
+
worktree_manager: nil,
|
|
16
|
+
ai_decision_engine: AIDecisionEngine.new,
|
|
17
|
+
label_config: {},
|
|
18
|
+
verbose: false,
|
|
19
|
+
shell_executor: nil
|
|
20
|
+
)
|
|
21
|
+
super(
|
|
22
|
+
repository_client: repository_client,
|
|
23
|
+
state_store: state_store,
|
|
24
|
+
label_config: label_config,
|
|
25
|
+
verbose: verbose
|
|
26
|
+
)
|
|
27
|
+
@worktree_manager = worktree_manager || Aidp::PRWorktreeManager.new
|
|
28
|
+
@ai_decision_engine = ai_decision_engine
|
|
29
|
+
@shell_executor = shell_executor || Aidp::ShellExecutor.new
|
|
30
|
+
@rebase_label = label_config[:rebase_trigger] ||
|
|
31
|
+
label_config["rebase_trigger"] ||
|
|
32
|
+
DEFAULT_REBASE_LABEL
|
|
33
|
+
|
|
34
|
+
Aidp.log_debug(
|
|
35
|
+
"rebase_processor",
|
|
36
|
+
"initialized",
|
|
37
|
+
rebase_label: @rebase_label
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def can_process?(work_item)
|
|
42
|
+
return false unless work_item.respond_to?(:pr?) && work_item.pr?
|
|
43
|
+
work_item.labels.include?(@rebase_label)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def process(work_item)
|
|
47
|
+
Aidp.log_debug(
|
|
48
|
+
"rebase_processor",
|
|
49
|
+
"processing_work_item",
|
|
50
|
+
pr_number: work_item.number,
|
|
51
|
+
branch: work_item.data[:head][:ref]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
# Fetch PR details
|
|
56
|
+
pr_details = @repository_client.get_pull_request(work_item.number)
|
|
57
|
+
base_branch = pr_details[:base][:ref]
|
|
58
|
+
head_branch = pr_details[:head][:ref]
|
|
59
|
+
|
|
60
|
+
# Create worktree for rebasing
|
|
61
|
+
worktree_path = @worktree_manager.create_pr_worktree(
|
|
62
|
+
pr_number: work_item.number,
|
|
63
|
+
base_branch: base_branch,
|
|
64
|
+
head_branch: head_branch
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Attempt rebase with intelligent conflict resolution
|
|
68
|
+
rebase_result = perform_rebase(worktree_path, base_branch, head_branch)
|
|
69
|
+
|
|
70
|
+
# Post results to PR
|
|
71
|
+
post_rebase_status(work_item.number, rebase_result)
|
|
72
|
+
|
|
73
|
+
Aidp.log_debug(
|
|
74
|
+
"rebase_processor",
|
|
75
|
+
"rebase_completed",
|
|
76
|
+
pr_number: work_item.number,
|
|
77
|
+
result: rebase_result
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
rebase_result
|
|
81
|
+
rescue => e
|
|
82
|
+
# Handle and log rebase failures
|
|
83
|
+
handle_rebase_error(work_item.number, e)
|
|
84
|
+
false
|
|
85
|
+
ensure
|
|
86
|
+
# Remove rebase label after processing (success or failure)
|
|
87
|
+
@repository_client.remove_labels(
|
|
88
|
+
work_item.number,
|
|
89
|
+
[@rebase_label]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Cleanup: remove temporary worktree
|
|
93
|
+
@worktree_manager.cleanup_pr_worktree(work_item.number)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def with_worktree_context(worktree_path)
|
|
100
|
+
if Dir.exist?(worktree_path)
|
|
101
|
+
Dir.chdir(worktree_path) { yield }
|
|
102
|
+
else
|
|
103
|
+
yield
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def perform_rebase(worktree_path, base_branch, head_branch)
|
|
108
|
+
Aidp.log_debug(
|
|
109
|
+
"rebase_processor",
|
|
110
|
+
"performing_rebase",
|
|
111
|
+
worktree_path: worktree_path,
|
|
112
|
+
base_branch: base_branch,
|
|
113
|
+
head_branch: head_branch
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Use PRWorktreeManager's method to perform rebase and conflict resolution
|
|
117
|
+
with_worktree_context(worktree_path) do
|
|
118
|
+
# Fetch the latest changes
|
|
119
|
+
@shell_executor.system("git fetch origin")
|
|
120
|
+
|
|
121
|
+
# Attempt to rebase
|
|
122
|
+
rebase_command = "git rebase origin/#{base_branch}"
|
|
123
|
+
rebase_output = @shell_executor.system(rebase_command)
|
|
124
|
+
|
|
125
|
+
unless rebase_output
|
|
126
|
+
# Conflict resolution using AI
|
|
127
|
+
conflict_files = detect_conflicting_files(worktree_path)
|
|
128
|
+
|
|
129
|
+
if !conflict_files.empty?
|
|
130
|
+
# Use AI-powered conflict resolution
|
|
131
|
+
resolution = resolve_conflicts(worktree_path, base_branch, conflict_files)
|
|
132
|
+
|
|
133
|
+
# If resolution is successful, continue with the rebase
|
|
134
|
+
if resolution
|
|
135
|
+
# Stage resolved files and continue rebase
|
|
136
|
+
# GIT_EDITOR=true prevents editor from opening during automated rebase
|
|
137
|
+
quoted_files = conflict_files.map { |f| "\"#{f}\"" }.join(" ")
|
|
138
|
+
@shell_executor.system("git add #{quoted_files}")
|
|
139
|
+
@shell_executor.system({"GIT_EDITOR" => "true"}, "git rebase --continue")
|
|
140
|
+
else
|
|
141
|
+
return false
|
|
142
|
+
end
|
|
143
|
+
else
|
|
144
|
+
# No conflicts, but rebase failed
|
|
145
|
+
return false
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Push the rebased branch
|
|
150
|
+
@shell_executor.system("git push -f origin #{head_branch}")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
true
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def detect_conflicting_files(worktree_path)
|
|
157
|
+
# Use git to list conflicting files
|
|
158
|
+
`cd #{worktree_path} && git diff --name-only --diff-filter=U`.split("\n")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def resolve_conflicts(worktree_path, base_branch, conflict_files)
|
|
162
|
+
Aidp.log_debug(
|
|
163
|
+
"rebase_processor",
|
|
164
|
+
"resolving_conflicts",
|
|
165
|
+
worktree_path: worktree_path,
|
|
166
|
+
base_branch: base_branch
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Use AIDecisionEngine to analyze and resolve merge conflicts
|
|
170
|
+
conflict_resolution = @ai_decision_engine.resolve_merge_conflict(
|
|
171
|
+
base_branch_path: worktree_path,
|
|
172
|
+
conflict_files: conflict_files
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
apply_conflict_resolution(worktree_path, conflict_resolution)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def apply_conflict_resolution(worktree_path, conflict_resolution)
|
|
179
|
+
Aidp.log_debug(
|
|
180
|
+
"rebase_processor",
|
|
181
|
+
"applying_conflict_resolution",
|
|
182
|
+
resolution: conflict_resolution
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Apply AI-generated conflict resolution
|
|
186
|
+
conflict_resolution.each do |file, resolution|
|
|
187
|
+
File.write(File.join(worktree_path, file), resolution)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Stage and continue rebase
|
|
191
|
+
# GIT_EDITOR=true prevents editor from opening during automated rebase
|
|
192
|
+
@shell_executor.system("cd #{worktree_path} && git add .")
|
|
193
|
+
@shell_executor.system({"GIT_EDITOR" => "true"}, "cd #{worktree_path} && git rebase --continue")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def post_rebase_status(pr_number, rebase_result, error_detail = nil)
|
|
197
|
+
status_method = rebase_result ? :add_success_status : :add_failure_status
|
|
198
|
+
description = if rebase_result
|
|
199
|
+
"PR successfully rebased"
|
|
200
|
+
elsif error_detail
|
|
201
|
+
error_detail
|
|
202
|
+
else
|
|
203
|
+
"Rebase failed"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
@repository_client.send(
|
|
207
|
+
status_method,
|
|
208
|
+
pr_number,
|
|
209
|
+
context: "aidp/rebase",
|
|
210
|
+
description: description
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
comment_text = if rebase_result
|
|
214
|
+
"✅ PR has been successfully rebased against the target branch."
|
|
215
|
+
elsif error_detail
|
|
216
|
+
"❌ Automatic rebase failed with error: `#{error_detail}`\n\n" \
|
|
217
|
+
"Please check the PR and rebase manually."
|
|
218
|
+
else
|
|
219
|
+
"❌ Automatic rebase failed. Manual intervention required."
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
@repository_client.post_comment(pr_number, comment_text)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def handle_rebase_error(pr_number, error)
|
|
226
|
+
Aidp.log_debug(
|
|
227
|
+
"rebase_processor",
|
|
228
|
+
"rebase_error",
|
|
229
|
+
pr_number: pr_number,
|
|
230
|
+
error_message: error.message,
|
|
231
|
+
error_class: error.class
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Extract the error message for the status
|
|
235
|
+
error_message = if error.is_a?(StandardError)
|
|
236
|
+
error.message
|
|
237
|
+
else
|
|
238
|
+
"Unknown error"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
post_rebase_status(pr_number, false, error_message)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "work_item"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Watch
|
|
7
|
+
# Schedules work items using a round-robin strategy across all tagged
|
|
8
|
+
# issues and PRs. Processes one iteration per work item before rotating
|
|
9
|
+
# to the next, ensuring fair distribution of attention.
|
|
10
|
+
#
|
|
11
|
+
# Priority handling:
|
|
12
|
+
# - aidp-plan items receive higher priority in rotation
|
|
13
|
+
# - Within the same priority, items are processed in queue order
|
|
14
|
+
#
|
|
15
|
+
# Paused items (aidp-needs-input label) are skipped during their turn
|
|
16
|
+
# but remain in the queue for later processing.
|
|
17
|
+
class RoundRobinScheduler
|
|
18
|
+
attr_reader :queue, :last_processed_key
|
|
19
|
+
|
|
20
|
+
def initialize(state_store:)
|
|
21
|
+
@state_store = state_store
|
|
22
|
+
@queue = []
|
|
23
|
+
@last_processed_key = state_store.round_robin_last_key
|
|
24
|
+
|
|
25
|
+
Aidp.log_debug("round_robin_scheduler", "initialized",
|
|
26
|
+
last_key: @last_processed_key)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Refresh the queue with current work items from GitHub.
|
|
30
|
+
# Maintains position in rotation when possible.
|
|
31
|
+
#
|
|
32
|
+
# @param work_items [Array<WorkItem>] Current work items from all processors
|
|
33
|
+
def refresh_queue(work_items)
|
|
34
|
+
Aidp.log_debug("round_robin_scheduler", "refresh_queue.start",
|
|
35
|
+
incoming_count: work_items.size,
|
|
36
|
+
current_queue_size: @queue.size)
|
|
37
|
+
|
|
38
|
+
# Sort by priority (primary) and key (secondary) for deterministic ordering
|
|
39
|
+
# This ensures consistent queue order across refreshes
|
|
40
|
+
@queue = work_items.sort_by { |item| [item.priority, item.key] }
|
|
41
|
+
|
|
42
|
+
Aidp.log_debug("round_robin_scheduler", "refresh_queue.complete",
|
|
43
|
+
queue_size: @queue.size,
|
|
44
|
+
priorities: @queue.map(&:priority).uniq.sort)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get the next work item to process using round-robin rotation.
|
|
48
|
+
# Skips paused items but keeps them in queue.
|
|
49
|
+
#
|
|
50
|
+
# @param paused_numbers [Array<Integer>] Issue/PR numbers that are paused
|
|
51
|
+
# @return [WorkItem, nil] Next item to process, or nil if none available
|
|
52
|
+
def next_item(paused_numbers: [])
|
|
53
|
+
return nil if @queue.empty?
|
|
54
|
+
|
|
55
|
+
Aidp.log_debug("round_robin_scheduler", "next_item.start",
|
|
56
|
+
queue_size: @queue.size,
|
|
57
|
+
paused_count: paused_numbers.size,
|
|
58
|
+
last_key: @last_processed_key)
|
|
59
|
+
|
|
60
|
+
# Find starting position based on last processed key
|
|
61
|
+
start_index = find_start_index
|
|
62
|
+
|
|
63
|
+
# Iterate through queue starting from rotation point
|
|
64
|
+
@queue.size.times do |offset|
|
|
65
|
+
index = (start_index + offset) % @queue.size
|
|
66
|
+
item = @queue[index]
|
|
67
|
+
|
|
68
|
+
# Skip paused items
|
|
69
|
+
if paused_numbers.include?(item.number)
|
|
70
|
+
Aidp.log_debug("round_robin_scheduler", "next_item.skip_paused",
|
|
71
|
+
key: item.key, number: item.number)
|
|
72
|
+
next
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Aidp.log_debug("round_robin_scheduler", "next_item.selected",
|
|
76
|
+
key: item.key, number: item.number, processor_type: item.processor_type)
|
|
77
|
+
|
|
78
|
+
return item
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
Aidp.log_debug("round_robin_scheduler", "next_item.all_paused",
|
|
82
|
+
queue_size: @queue.size)
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Mark a work item as processed and persist the rotation state.
|
|
87
|
+
#
|
|
88
|
+
# @param item [WorkItem] The item that was just processed
|
|
89
|
+
def mark_processed(item)
|
|
90
|
+
@last_processed_key = item.key
|
|
91
|
+
@state_store.record_round_robin_position(
|
|
92
|
+
last_key: item.key,
|
|
93
|
+
processed_at: Time.now.utc.iso8601
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
Aidp.log_debug("round_robin_scheduler", "mark_processed",
|
|
97
|
+
key: item.key, number: item.number)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if there are any non-paused items in the queue.
|
|
101
|
+
#
|
|
102
|
+
# @param paused_numbers [Array<Integer>] Issue/PR numbers that are paused
|
|
103
|
+
# @return [Boolean] True if there's work to do
|
|
104
|
+
def work?(paused_numbers: [])
|
|
105
|
+
@queue.any? { |item| !paused_numbers.include?(item.number) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get queue statistics for debugging/monitoring.
|
|
109
|
+
#
|
|
110
|
+
# @return [Hash] Queue statistics
|
|
111
|
+
def stats
|
|
112
|
+
by_processor = @queue.group_by(&:processor_type)
|
|
113
|
+
by_priority = @queue.group_by(&:priority)
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
total: @queue.size,
|
|
117
|
+
by_processor: by_processor.transform_values(&:size),
|
|
118
|
+
by_priority: by_priority.transform_values(&:size),
|
|
119
|
+
last_processed_key: @last_processed_key
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# Find the index to start searching from based on last processed item.
|
|
126
|
+
# Returns the index AFTER the last processed item for round-robin.
|
|
127
|
+
#
|
|
128
|
+
# Empty queue safety: This method returns 0 for empty queues, but the
|
|
129
|
+
# caller (next_item) guards against empty queues before calling this method.
|
|
130
|
+
# The modulo operation is safe because we've verified queue.size > 0.
|
|
131
|
+
#
|
|
132
|
+
# @return [Integer] Starting index for rotation (0 if queue is empty,
|
|
133
|
+
# no last key, or last processed item not found)
|
|
134
|
+
def find_start_index
|
|
135
|
+
return 0 if @last_processed_key.nil?
|
|
136
|
+
return 0 if @queue.empty?
|
|
137
|
+
|
|
138
|
+
# Find the last processed item in the current queue
|
|
139
|
+
last_index = @queue.find_index { |item| item.key == @last_processed_key }
|
|
140
|
+
|
|
141
|
+
# If not found (item was removed), start from beginning
|
|
142
|
+
return 0 unless last_index
|
|
143
|
+
|
|
144
|
+
# Start from the next item (wrap around)
|
|
145
|
+
# Safe: @queue.size > 0 guaranteed by early return on line 136
|
|
146
|
+
(last_index + 1) % @queue.size
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|