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,643 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Watch
|
|
8
|
+
# Handles reconciliation of dirty worktrees (those with uncommitted changes).
|
|
9
|
+
# Determines appropriate action based on linked issue/PR state:
|
|
10
|
+
# - Open issue/PR: Resume work by triggering build processor
|
|
11
|
+
# - Merged PR: Reconcile changes and create follow-up PR if meaningful diff remains
|
|
12
|
+
# - Closed without merge: Log and optionally clean up
|
|
13
|
+
#
|
|
14
|
+
# This runs as part of the watch mode poll cycle, after cleanup but before sleep.
|
|
15
|
+
class WorktreeReconciler
|
|
16
|
+
# Pattern to extract issue number from worktree slug
|
|
17
|
+
# e.g., "issue-375-thinking-tiers-escalate" -> 375
|
|
18
|
+
ISSUE_SLUG_PATTERN = /^issue-(\d+)-/
|
|
19
|
+
|
|
20
|
+
# Pattern to extract PR number from worktree slug
|
|
21
|
+
# e.g., "pr-456-ci-fix" -> 456
|
|
22
|
+
PR_SLUG_PATTERN = /^pr-(\d+)-/
|
|
23
|
+
|
|
24
|
+
# @param project_dir [String] Project root directory
|
|
25
|
+
# @param repository_client [RepositoryClient] GitHub API client
|
|
26
|
+
# @param build_processor [BuildProcessor] Processor for resuming builds
|
|
27
|
+
# @param state_store [StateStore] For tracking build state
|
|
28
|
+
# @param config [Hash] Configuration options
|
|
29
|
+
def initialize(project_dir:, repository_client:, build_processor:, state_store:, config: {})
|
|
30
|
+
@project_dir = project_dir
|
|
31
|
+
@repository_client = repository_client
|
|
32
|
+
@build_processor = build_processor
|
|
33
|
+
@state_store = state_store
|
|
34
|
+
@config = normalize_config(config)
|
|
35
|
+
|
|
36
|
+
Aidp.log_debug("worktree_reconciler", "initialized",
|
|
37
|
+
project_dir: project_dir,
|
|
38
|
+
config: @config)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if reconciliation is due based on last run time
|
|
42
|
+
#
|
|
43
|
+
# @param last_reconcile_at [Time, nil] Time of last reconciliation run
|
|
44
|
+
# @return [Boolean] True if reconciliation should run
|
|
45
|
+
def reconciliation_due?(last_reconcile_at)
|
|
46
|
+
return false unless enabled?
|
|
47
|
+
return true if last_reconcile_at.nil?
|
|
48
|
+
|
|
49
|
+
elapsed = Time.now - last_reconcile_at
|
|
50
|
+
elapsed >= reconciliation_interval_seconds
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Execute reconciliation of dirty worktrees
|
|
54
|
+
#
|
|
55
|
+
# @return [Hash] Result containing resumed, reconciled, skipped counts and errors
|
|
56
|
+
def execute
|
|
57
|
+
Aidp.log_info("worktree_reconciler", "reconciliation_started")
|
|
58
|
+
|
|
59
|
+
return {resumed: 0, reconciled: 0, cleaned: 0, skipped: 0, errors: []} unless enabled?
|
|
60
|
+
|
|
61
|
+
worktrees = list_dirty_worktrees
|
|
62
|
+
Aidp.log_debug("worktree_reconciler", "dirty_worktrees_found", count: worktrees.size)
|
|
63
|
+
|
|
64
|
+
results = {resumed: 0, reconciled: 0, cleaned: 0, skipped: 0, errors: []}
|
|
65
|
+
|
|
66
|
+
worktrees.each do |worktree|
|
|
67
|
+
result = process_dirty_worktree(worktree)
|
|
68
|
+
case result[:action]
|
|
69
|
+
when :resumed
|
|
70
|
+
results[:resumed] += 1
|
|
71
|
+
when :reconciled
|
|
72
|
+
results[:reconciled] += 1
|
|
73
|
+
when :cleaned
|
|
74
|
+
results[:cleaned] += 1
|
|
75
|
+
when :skipped
|
|
76
|
+
results[:skipped] += 1
|
|
77
|
+
when :error
|
|
78
|
+
results[:errors] << {slug: worktree[:slug], error: result[:error]}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
Aidp.log_info("worktree_reconciler", "reconciliation_completed",
|
|
83
|
+
resumed: results[:resumed],
|
|
84
|
+
reconciled: results[:reconciled],
|
|
85
|
+
cleaned: results[:cleaned],
|
|
86
|
+
skipped: results[:skipped],
|
|
87
|
+
errors_count: results[:errors].size)
|
|
88
|
+
|
|
89
|
+
results
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Check if reconciliation is enabled
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
def enabled?
|
|
96
|
+
@config[:enabled]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get the configured reconciliation interval in seconds
|
|
100
|
+
#
|
|
101
|
+
# @return [Integer]
|
|
102
|
+
def reconciliation_interval_seconds
|
|
103
|
+
@config[:interval_seconds]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def normalize_config(config)
|
|
109
|
+
{
|
|
110
|
+
enabled: config.fetch(:enabled, config.fetch("enabled", true)),
|
|
111
|
+
# Default to checking every 5 minutes
|
|
112
|
+
interval_seconds: config.fetch(:interval_seconds, config.fetch("interval_seconds", 300)),
|
|
113
|
+
base_branch: config.fetch(:base_branch, config.fetch("base_branch", "main")),
|
|
114
|
+
# Auto-resume open issues by default
|
|
115
|
+
auto_resume: config.fetch(:auto_resume, config.fetch("auto_resume", true)),
|
|
116
|
+
# Auto-reconcile merged PRs by default
|
|
117
|
+
auto_reconcile: config.fetch(:auto_reconcile, config.fetch("auto_reconcile", true))
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def base_branch
|
|
122
|
+
@config[:base_branch]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# List all worktrees that have uncommitted changes
|
|
126
|
+
#
|
|
127
|
+
# @return [Array<Hash>] Worktrees with uncommitted changes
|
|
128
|
+
def list_dirty_worktrees
|
|
129
|
+
all_worktrees = Aidp::Worktree.list(project_dir: @project_dir)
|
|
130
|
+
|
|
131
|
+
all_worktrees.select do |worktree|
|
|
132
|
+
next false unless worktree[:active]
|
|
133
|
+
next false unless Dir.exist?(worktree[:path])
|
|
134
|
+
|
|
135
|
+
!worktree_clean?(worktree[:path])
|
|
136
|
+
end
|
|
137
|
+
rescue => e
|
|
138
|
+
Aidp.log_error("worktree_reconciler", "list_worktrees_failed", error: e.message)
|
|
139
|
+
[]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def worktree_clean?(path)
|
|
143
|
+
stdout, _stderr, status = Open3.capture3(
|
|
144
|
+
"git", "status", "--porcelain",
|
|
145
|
+
chdir: path
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
status.success? && stdout.strip.empty?
|
|
149
|
+
rescue => e
|
|
150
|
+
Aidp.log_error("worktree_reconciler", "check_clean_failed",
|
|
151
|
+
path: path,
|
|
152
|
+
error: e.message)
|
|
153
|
+
true # Assume clean on error to avoid false positives
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Process a single dirty worktree
|
|
157
|
+
#
|
|
158
|
+
# @param worktree [Hash] Worktree info from registry
|
|
159
|
+
# @return [Hash] Result with :action and optional :error
|
|
160
|
+
def process_dirty_worktree(worktree)
|
|
161
|
+
slug = worktree[:slug]
|
|
162
|
+
branch = worktree[:branch]
|
|
163
|
+
|
|
164
|
+
Aidp.log_debug("worktree_reconciler", "processing_dirty_worktree",
|
|
165
|
+
slug: slug,
|
|
166
|
+
branch: branch)
|
|
167
|
+
|
|
168
|
+
# Determine what this worktree is for
|
|
169
|
+
issue_number = extract_issue_number(slug)
|
|
170
|
+
pr_number = extract_pr_number(slug) || find_pr_for_branch(branch)
|
|
171
|
+
|
|
172
|
+
if pr_number
|
|
173
|
+
process_pr_worktree(worktree, pr_number, issue_number)
|
|
174
|
+
elsif issue_number
|
|
175
|
+
process_issue_worktree(worktree, issue_number)
|
|
176
|
+
else
|
|
177
|
+
Aidp.log_debug("worktree_reconciler", "orphan_worktree",
|
|
178
|
+
slug: slug,
|
|
179
|
+
reason: "no_linked_issue_or_pr")
|
|
180
|
+
{action: :skipped, reason: "orphan_worktree"}
|
|
181
|
+
end
|
|
182
|
+
rescue => e
|
|
183
|
+
Aidp.log_error("worktree_reconciler", "process_worktree_error",
|
|
184
|
+
slug: slug,
|
|
185
|
+
error: e.message)
|
|
186
|
+
{action: :error, error: e.message}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Process a worktree linked to a PR
|
|
190
|
+
#
|
|
191
|
+
# @param worktree [Hash] Worktree info
|
|
192
|
+
# @param pr_number [Integer] PR number
|
|
193
|
+
# @param issue_number [Integer, nil] Linked issue number if known
|
|
194
|
+
# @return [Hash] Result
|
|
195
|
+
def process_pr_worktree(worktree, pr_number, issue_number)
|
|
196
|
+
pr = fetch_pr_state(pr_number)
|
|
197
|
+
|
|
198
|
+
unless pr
|
|
199
|
+
Aidp.log_debug("worktree_reconciler", "pr_not_found",
|
|
200
|
+
pr_number: pr_number,
|
|
201
|
+
slug: worktree[:slug])
|
|
202
|
+
return {action: :skipped, reason: "pr_not_found"}
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
case pr[:state]
|
|
206
|
+
when "MERGED"
|
|
207
|
+
reconcile_merged_pr(worktree, pr, issue_number)
|
|
208
|
+
when "OPEN"
|
|
209
|
+
resume_open_pr(worktree, pr, issue_number)
|
|
210
|
+
when "CLOSED"
|
|
211
|
+
handle_closed_pr(worktree, pr)
|
|
212
|
+
else
|
|
213
|
+
{action: :skipped, reason: "unknown_pr_state"}
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Process a worktree linked to an issue (no PR yet)
|
|
218
|
+
#
|
|
219
|
+
# @param worktree [Hash] Worktree info
|
|
220
|
+
# @param issue_number [Integer] Issue number
|
|
221
|
+
# @return [Hash] Result
|
|
222
|
+
def process_issue_worktree(worktree, issue_number)
|
|
223
|
+
issue = fetch_issue_state(issue_number)
|
|
224
|
+
|
|
225
|
+
unless issue
|
|
226
|
+
Aidp.log_debug("worktree_reconciler", "issue_not_found",
|
|
227
|
+
issue_number: issue_number,
|
|
228
|
+
slug: worktree[:slug])
|
|
229
|
+
return {action: :skipped, reason: "issue_not_found"}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if issue[:state] == "OPEN"
|
|
233
|
+
resume_open_issue(worktree, issue)
|
|
234
|
+
else
|
|
235
|
+
Aidp.log_debug("worktree_reconciler", "issue_closed",
|
|
236
|
+
issue_number: issue_number,
|
|
237
|
+
slug: worktree[:slug])
|
|
238
|
+
# Issue is closed but we have uncommitted changes
|
|
239
|
+
# This could be work that was done but never pushed
|
|
240
|
+
{action: :skipped, reason: "issue_closed_with_uncommitted_changes"}
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Reconcile a worktree whose PR has been merged
|
|
245
|
+
#
|
|
246
|
+
# @param worktree [Hash] Worktree info
|
|
247
|
+
# @param pr [Hash] PR data
|
|
248
|
+
# @param issue_number [Integer, nil] Linked issue number
|
|
249
|
+
# @return [Hash] Result
|
|
250
|
+
def reconcile_merged_pr(worktree, pr, issue_number)
|
|
251
|
+
return {action: :skipped, reason: "auto_reconcile_disabled"} unless @config[:auto_reconcile]
|
|
252
|
+
|
|
253
|
+
slug = worktree[:slug]
|
|
254
|
+
path = worktree[:path]
|
|
255
|
+
|
|
256
|
+
Aidp.log_info("worktree_reconciler", "reconciling_merged_pr",
|
|
257
|
+
slug: slug,
|
|
258
|
+
pr_number: pr[:number])
|
|
259
|
+
|
|
260
|
+
# Fetch latest base branch to compare against
|
|
261
|
+
fetch_base_branch(path)
|
|
262
|
+
|
|
263
|
+
# Get diff between worktree and merged base branch
|
|
264
|
+
remaining_changes = get_remaining_diff(path, pr[:base_branch] || base_branch)
|
|
265
|
+
|
|
266
|
+
if remaining_changes.empty?
|
|
267
|
+
# No meaningful changes remain - clean up
|
|
268
|
+
Aidp.log_info("worktree_reconciler", "no_remaining_changes",
|
|
269
|
+
slug: slug,
|
|
270
|
+
pr_number: pr[:number])
|
|
271
|
+
cleanup_worktree(slug)
|
|
272
|
+
{action: :cleaned, reason: "no_remaining_changes_after_merge"}
|
|
273
|
+
else
|
|
274
|
+
# There are changes that weren't in the merged PR
|
|
275
|
+
Aidp.log_info("worktree_reconciler", "remaining_changes_found",
|
|
276
|
+
slug: slug,
|
|
277
|
+
pr_number: pr[:number],
|
|
278
|
+
files_changed: remaining_changes.size)
|
|
279
|
+
create_followup_pr(worktree, pr, remaining_changes, issue_number)
|
|
280
|
+
{action: :reconciled, reason: "followup_pr_created"}
|
|
281
|
+
end
|
|
282
|
+
rescue => e
|
|
283
|
+
Aidp.log_error("worktree_reconciler", "reconcile_merged_pr_failed",
|
|
284
|
+
slug: slug,
|
|
285
|
+
error: e.message)
|
|
286
|
+
{action: :error, error: e.message}
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Resume work on an open PR
|
|
290
|
+
#
|
|
291
|
+
# @param worktree [Hash] Worktree info
|
|
292
|
+
# @param pr [Hash] PR data
|
|
293
|
+
# @param issue_number [Integer, nil] Linked issue number
|
|
294
|
+
# @return [Hash] Result
|
|
295
|
+
def resume_open_pr(worktree, pr, issue_number)
|
|
296
|
+
return {action: :skipped, reason: "auto_resume_disabled"} unless @config[:auto_resume]
|
|
297
|
+
|
|
298
|
+
# For open PRs with uncommitted changes, we just log it
|
|
299
|
+
# The change-request processor should handle this if there are pending changes
|
|
300
|
+
Aidp.log_info("worktree_reconciler", "open_pr_with_uncommitted_changes",
|
|
301
|
+
slug: worktree[:slug],
|
|
302
|
+
pr_number: pr[:number])
|
|
303
|
+
|
|
304
|
+
# If we have a linked issue, try to resume via build processor
|
|
305
|
+
if issue_number
|
|
306
|
+
resume_via_build_processor(worktree, issue_number)
|
|
307
|
+
else
|
|
308
|
+
{action: :skipped, reason: "open_pr_needs_manual_attention"}
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Resume work on an open issue
|
|
313
|
+
#
|
|
314
|
+
# @param worktree [Hash] Worktree info
|
|
315
|
+
# @param issue [Hash] Issue data
|
|
316
|
+
# @return [Hash] Result
|
|
317
|
+
def resume_open_issue(worktree, issue)
|
|
318
|
+
return {action: :skipped, reason: "auto_resume_disabled"} unless @config[:auto_resume]
|
|
319
|
+
|
|
320
|
+
# Check if issue has the build label
|
|
321
|
+
has_build_label = issue[:labels]&.any? { |l| l["name"] == @build_processor.build_label }
|
|
322
|
+
|
|
323
|
+
if has_build_label
|
|
324
|
+
resume_via_build_processor(worktree, issue[:number])
|
|
325
|
+
else
|
|
326
|
+
Aidp.log_debug("worktree_reconciler", "issue_missing_build_label",
|
|
327
|
+
issue_number: issue[:number],
|
|
328
|
+
slug: worktree[:slug])
|
|
329
|
+
{action: :skipped, reason: "issue_missing_build_label"}
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Handle a closed (not merged) PR
|
|
334
|
+
#
|
|
335
|
+
# @param worktree [Hash] Worktree info
|
|
336
|
+
# @param pr [Hash] PR data
|
|
337
|
+
# @return [Hash] Result
|
|
338
|
+
def handle_closed_pr(worktree, pr)
|
|
339
|
+
Aidp.log_info("worktree_reconciler", "closed_pr_with_uncommitted_changes",
|
|
340
|
+
slug: worktree[:slug],
|
|
341
|
+
pr_number: pr[:number])
|
|
342
|
+
|
|
343
|
+
# Don't automatically delete - user might want to review the work
|
|
344
|
+
{action: :skipped, reason: "pr_closed_without_merge"}
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Resume work via the build processor
|
|
348
|
+
#
|
|
349
|
+
# @param worktree [Hash] Worktree info
|
|
350
|
+
# @param issue_number [Integer] Issue number
|
|
351
|
+
# @return [Hash] Result
|
|
352
|
+
def resume_via_build_processor(worktree, issue_number)
|
|
353
|
+
Aidp.log_info("worktree_reconciler", "resuming_work",
|
|
354
|
+
slug: worktree[:slug],
|
|
355
|
+
issue_number: issue_number)
|
|
356
|
+
|
|
357
|
+
# Fetch the full issue details
|
|
358
|
+
issue = @repository_client.fetch_issue(issue_number)
|
|
359
|
+
|
|
360
|
+
unless issue
|
|
361
|
+
return {action: :error, error: "failed_to_fetch_issue"}
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Trigger the build processor
|
|
365
|
+
@build_processor.process(issue)
|
|
366
|
+
|
|
367
|
+
{action: :resumed, reason: "build_processor_triggered"}
|
|
368
|
+
rescue => e
|
|
369
|
+
Aidp.log_error("worktree_reconciler", "resume_failed",
|
|
370
|
+
slug: worktree[:slug],
|
|
371
|
+
issue_number: issue_number,
|
|
372
|
+
error: e.message)
|
|
373
|
+
{action: :error, error: e.message}
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Extract issue number from worktree slug
|
|
377
|
+
#
|
|
378
|
+
# @param slug [String] Worktree slug
|
|
379
|
+
# @return [Integer, nil] Issue number or nil
|
|
380
|
+
def extract_issue_number(slug)
|
|
381
|
+
match = slug.match(ISSUE_SLUG_PATTERN)
|
|
382
|
+
match ? match[1].to_i : nil
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Extract PR number from worktree slug
|
|
386
|
+
#
|
|
387
|
+
# @param slug [String] Worktree slug
|
|
388
|
+
# @return [Integer, nil] PR number or nil
|
|
389
|
+
def extract_pr_number(slug)
|
|
390
|
+
match = slug.match(PR_SLUG_PATTERN)
|
|
391
|
+
match ? match[1].to_i : nil
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Find PR number for a given branch
|
|
395
|
+
#
|
|
396
|
+
# @param branch [String] Branch name
|
|
397
|
+
# @return [Integer, nil] PR number or nil
|
|
398
|
+
def find_pr_for_branch(branch)
|
|
399
|
+
# Use gh CLI to find PR for this branch
|
|
400
|
+
stdout, _stderr, status = Open3.capture3(
|
|
401
|
+
"gh", "pr", "list",
|
|
402
|
+
"--repo", @repository_client.full_repo,
|
|
403
|
+
"--head", branch,
|
|
404
|
+
"--state", "all",
|
|
405
|
+
"--json", "number",
|
|
406
|
+
"--limit", "1"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
return nil unless status.success?
|
|
410
|
+
|
|
411
|
+
prs = JSON.parse(stdout)
|
|
412
|
+
prs.first&.dig("number")
|
|
413
|
+
rescue => e
|
|
414
|
+
Aidp.log_debug("worktree_reconciler", "find_pr_for_branch_failed",
|
|
415
|
+
branch: branch,
|
|
416
|
+
error: e.message)
|
|
417
|
+
nil
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Fetch PR state from GitHub
|
|
421
|
+
#
|
|
422
|
+
# @param pr_number [Integer] PR number
|
|
423
|
+
# @return [Hash, nil] PR data or nil
|
|
424
|
+
def fetch_pr_state(pr_number)
|
|
425
|
+
stdout, _stderr, status = Open3.capture3(
|
|
426
|
+
"gh", "pr", "view", pr_number.to_s,
|
|
427
|
+
"--repo", @repository_client.full_repo,
|
|
428
|
+
"--json", "number,state,mergedAt,baseRefName,headRefName,title"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
return nil unless status.success?
|
|
432
|
+
|
|
433
|
+
data = JSON.parse(stdout)
|
|
434
|
+
{
|
|
435
|
+
number: data["number"],
|
|
436
|
+
state: data["state"],
|
|
437
|
+
merged_at: data["mergedAt"],
|
|
438
|
+
base_branch: data["baseRefName"],
|
|
439
|
+
head_branch: data["headRefName"],
|
|
440
|
+
title: data["title"]
|
|
441
|
+
}
|
|
442
|
+
rescue => e
|
|
443
|
+
Aidp.log_debug("worktree_reconciler", "fetch_pr_state_failed",
|
|
444
|
+
pr_number: pr_number,
|
|
445
|
+
error: e.message)
|
|
446
|
+
nil
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Fetch issue state from GitHub
|
|
450
|
+
#
|
|
451
|
+
# @param issue_number [Integer] Issue number
|
|
452
|
+
# @return [Hash, nil] Issue data or nil
|
|
453
|
+
def fetch_issue_state(issue_number)
|
|
454
|
+
stdout, _stderr, status = Open3.capture3(
|
|
455
|
+
"gh", "issue", "view", issue_number.to_s,
|
|
456
|
+
"--repo", @repository_client.full_repo,
|
|
457
|
+
"--json", "number,state,title,labels"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
return nil unless status.success?
|
|
461
|
+
|
|
462
|
+
data = JSON.parse(stdout)
|
|
463
|
+
{
|
|
464
|
+
number: data["number"],
|
|
465
|
+
state: data["state"],
|
|
466
|
+
title: data["title"],
|
|
467
|
+
labels: data["labels"]
|
|
468
|
+
}
|
|
469
|
+
rescue => e
|
|
470
|
+
Aidp.log_debug("worktree_reconciler", "fetch_issue_state_failed",
|
|
471
|
+
issue_number: issue_number,
|
|
472
|
+
error: e.message)
|
|
473
|
+
nil
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Fetch the latest base branch in the worktree
|
|
477
|
+
#
|
|
478
|
+
# @param path [String] Worktree path
|
|
479
|
+
def fetch_base_branch(path)
|
|
480
|
+
Open3.capture3("git", "fetch", "origin", base_branch, chdir: path)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Get list of files with remaining changes after merge
|
|
484
|
+
#
|
|
485
|
+
# @param path [String] Worktree path
|
|
486
|
+
# @param target_branch [String] Branch to compare against
|
|
487
|
+
# @return [Array<String>] List of changed files
|
|
488
|
+
def get_remaining_diff(path, target_branch)
|
|
489
|
+
# First, get list of modified/untracked files in worktree
|
|
490
|
+
stdout, _stderr, status = Open3.capture3(
|
|
491
|
+
"git", "status", "--porcelain",
|
|
492
|
+
chdir: path
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
return [] unless status.success?
|
|
496
|
+
|
|
497
|
+
changed_files = stdout.lines.map { |line| line[3..].strip }.reject(&:empty?)
|
|
498
|
+
return [] if changed_files.empty?
|
|
499
|
+
|
|
500
|
+
# For each changed file, check if it differs from the target branch
|
|
501
|
+
remaining = []
|
|
502
|
+
changed_files.each do |file|
|
|
503
|
+
# Check if file content differs from target branch
|
|
504
|
+
diff_out, _diff_err, diff_status = Open3.capture3(
|
|
505
|
+
"git", "diff", "origin/#{target_branch}", "--", file,
|
|
506
|
+
chdir: path
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# If diff is non-empty, this file has changes not in target branch
|
|
510
|
+
remaining << file if diff_status.success? && !diff_out.strip.empty?
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
remaining
|
|
514
|
+
rescue => e
|
|
515
|
+
Aidp.log_error("worktree_reconciler", "get_remaining_diff_failed",
|
|
516
|
+
path: path,
|
|
517
|
+
error: e.message)
|
|
518
|
+
[]
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# Create a follow-up PR for remaining changes
|
|
522
|
+
#
|
|
523
|
+
# @param worktree [Hash] Worktree info
|
|
524
|
+
# @param original_pr [Hash] Original merged PR data
|
|
525
|
+
# @param changed_files [Array<String>] Files with changes
|
|
526
|
+
# @param issue_number [Integer, nil] Linked issue number
|
|
527
|
+
def create_followup_pr(worktree, original_pr, changed_files, issue_number)
|
|
528
|
+
path = worktree[:path]
|
|
529
|
+
slug = worktree[:slug]
|
|
530
|
+
|
|
531
|
+
Aidp.log_info("worktree_reconciler", "creating_followup_pr",
|
|
532
|
+
slug: slug,
|
|
533
|
+
original_pr: original_pr[:number],
|
|
534
|
+
changed_files: changed_files.size)
|
|
535
|
+
|
|
536
|
+
# Create a new branch for the follow-up
|
|
537
|
+
followup_branch = "#{worktree[:branch]}-followup-#{Time.now.to_i}"
|
|
538
|
+
|
|
539
|
+
Dir.chdir(path) do
|
|
540
|
+
# Checkout a new branch from current state
|
|
541
|
+
Open3.capture3("git", "checkout", "-b", followup_branch)
|
|
542
|
+
|
|
543
|
+
# Stage all changes
|
|
544
|
+
Open3.capture3("git", "add", "-A")
|
|
545
|
+
|
|
546
|
+
# Build commit message parts
|
|
547
|
+
issue_ref = issue_number ? "Related issue: ##{issue_number}\n" : ""
|
|
548
|
+
files_list = changed_files.map { |f| "- #{f}" }.join("\n")
|
|
549
|
+
|
|
550
|
+
commit_message = <<~MSG
|
|
551
|
+
Follow-up changes from PR ##{original_pr[:number]}
|
|
552
|
+
|
|
553
|
+
These changes were found in a local worktree after PR ##{original_pr[:number]}
|
|
554
|
+
was merged from another machine. This follow-up PR captures the remaining
|
|
555
|
+
uncommitted work.
|
|
556
|
+
|
|
557
|
+
Original PR: ##{original_pr[:number]} - #{original_pr[:title]}
|
|
558
|
+
#{issue_ref}
|
|
559
|
+
Changed files:
|
|
560
|
+
#{files_list}
|
|
561
|
+
|
|
562
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
563
|
+
|
|
564
|
+
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
|
565
|
+
MSG
|
|
566
|
+
|
|
567
|
+
Open3.capture3("git", "commit", "-m", commit_message)
|
|
568
|
+
|
|
569
|
+
# Push the branch
|
|
570
|
+
Open3.capture3("git", "push", "-u", "origin", followup_branch)
|
|
571
|
+
|
|
572
|
+
# Build PR body parts
|
|
573
|
+
issue_context = issue_number ? "- Related issue: ##{issue_number}\n" : ""
|
|
574
|
+
files_md = changed_files.map { |f| "- `#{f}`" }.join("\n")
|
|
575
|
+
|
|
576
|
+
pr_body = <<~BODY
|
|
577
|
+
## Summary
|
|
578
|
+
|
|
579
|
+
This PR captures follow-up changes that were found in a local worktree after
|
|
580
|
+
PR ##{original_pr[:number]} was merged from another machine.
|
|
581
|
+
|
|
582
|
+
## Context
|
|
583
|
+
|
|
584
|
+
- Original PR: ##{original_pr[:number]}
|
|
585
|
+
#{issue_context}
|
|
586
|
+
## Changed Files
|
|
587
|
+
|
|
588
|
+
#{files_md}
|
|
589
|
+
|
|
590
|
+
## Review Notes
|
|
591
|
+
|
|
592
|
+
Please review these changes carefully - they represent uncommitted work that
|
|
593
|
+
was not included in the original PR merge.
|
|
594
|
+
|
|
595
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
596
|
+
BODY
|
|
597
|
+
|
|
598
|
+
pr_title = "Follow-up: Additional changes from PR ##{original_pr[:number]}"
|
|
599
|
+
|
|
600
|
+
stdout, stderr, status = Open3.capture3(
|
|
601
|
+
"gh", "pr", "create",
|
|
602
|
+
"--repo", @repository_client.full_repo,
|
|
603
|
+
"--title", pr_title,
|
|
604
|
+
"--body", pr_body,
|
|
605
|
+
"--base", original_pr[:base_branch] || base_branch,
|
|
606
|
+
"--head", followup_branch
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
if status.success?
|
|
610
|
+
Aidp.log_info("worktree_reconciler", "followup_pr_created",
|
|
611
|
+
slug: slug,
|
|
612
|
+
pr_url: stdout.strip)
|
|
613
|
+
else
|
|
614
|
+
Aidp.log_error("worktree_reconciler", "followup_pr_creation_failed",
|
|
615
|
+
slug: slug,
|
|
616
|
+
error: stderr)
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
rescue => e
|
|
620
|
+
Aidp.log_error("worktree_reconciler", "create_followup_pr_failed",
|
|
621
|
+
slug: slug,
|
|
622
|
+
error: e.message)
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# Clean up a worktree
|
|
626
|
+
#
|
|
627
|
+
# @param slug [String] Worktree slug
|
|
628
|
+
def cleanup_worktree(slug)
|
|
629
|
+
Aidp.log_info("worktree_reconciler", "cleaning_up_worktree", slug: slug)
|
|
630
|
+
|
|
631
|
+
Aidp::Worktree.remove(
|
|
632
|
+
slug: slug,
|
|
633
|
+
project_dir: @project_dir,
|
|
634
|
+
delete_branch: true
|
|
635
|
+
)
|
|
636
|
+
rescue => e
|
|
637
|
+
Aidp.log_error("worktree_reconciler", "cleanup_failed",
|
|
638
|
+
slug: slug,
|
|
639
|
+
error: e.message)
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
end
|
|
@@ -38,7 +38,7 @@ module Aidp
|
|
|
38
38
|
@conversation_history = []
|
|
39
39
|
@user_input = {}
|
|
40
40
|
@verbose = verbose
|
|
41
|
-
@debug_env =
|
|
41
|
+
@debug_env = debug_level >= Aidp::DebugMixin::DEBUG_BASIC
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# Main entry point for guided workflow selection
|