aidp 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/aidp/config.rb +2 -8
- data/lib/aidp/harness/configuration.rb +1 -1
- data/lib/aidp/harness/provider_manager.rb +36 -5
- data/lib/aidp/providers/anthropic.rb +17 -14
- data/lib/aidp/providers/cursor.rb +7 -1
- data/lib/aidp/setup/wizard.rb +65 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/auto_pr_processor.rb +86 -0
- data/lib/aidp/watch/auto_processor.rb +78 -0
- data/lib/aidp/watch/change_request_processor.rb +105 -27
- data/lib/aidp/watch/runner.rb +104 -0
- data/lib/aidp/watch/state_store.rb +37 -0
- data/lib/aidp/worktree_branch_manager.rb +147 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de5176199a74d1e4992451708e3830fff889cffd526c8bc0887a0aa73a4946a9
|
|
4
|
+
data.tar.gz: 399593a4d2b8d6991d37d22f383c3cd816b5b276b42e1aa45b0a2df064a470da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d290891c58232da0b32ca1a8560ba211f5a5805c8aa7b24d78f1536cd4e32750ca9ee11dbb8c1b25f018ed50082c5fd52e9412f7b871981f0ff8eacef6f5e15
|
|
7
|
+
data.tar.gz: c10ee2a196880353991978cdf7a92775e13828b82452ba323c152853740a402a07bad29dad8984da1f9f0e2f650e5075bde9308ac8cc326b89de72af66652b2a
|
data/lib/aidp/config.rb
CHANGED
|
@@ -111,11 +111,10 @@ module Aidp
|
|
|
111
111
|
model_family: "claude",
|
|
112
112
|
max_tokens: 100_000,
|
|
113
113
|
default_flags: ["--dangerously-skip-permissions"],
|
|
114
|
-
models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"
|
|
114
|
+
models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"],
|
|
115
115
|
model_weights: {
|
|
116
116
|
"claude-3-5-sonnet-20241022" => 3,
|
|
117
|
-
"claude-3-5-haiku-20241022" => 2
|
|
118
|
-
"claude-3-opus-20240229" => 1
|
|
117
|
+
"claude-3-5-haiku-20241022" => 2
|
|
119
118
|
},
|
|
120
119
|
models_config: {
|
|
121
120
|
"claude-3-5-sonnet-20241022" => {
|
|
@@ -127,11 +126,6 @@ module Aidp
|
|
|
127
126
|
flags: ["--dangerously-skip-permissions"],
|
|
128
127
|
max_tokens: 200_000,
|
|
129
128
|
timeout: 180
|
|
130
|
-
},
|
|
131
|
-
"claude-3-opus-20240229" => {
|
|
132
|
-
flags: ["--dangerously-skip-permissions"],
|
|
133
|
-
max_tokens: 200_000,
|
|
134
|
-
timeout: 600
|
|
135
129
|
}
|
|
136
130
|
},
|
|
137
131
|
auth: {
|
|
@@ -1057,7 +1057,7 @@ module Aidp
|
|
|
1057
1057
|
def default_thinking_config
|
|
1058
1058
|
{
|
|
1059
1059
|
default_tier: "mini", # Use mini tier by default for cost optimization
|
|
1060
|
-
max_tier: "
|
|
1060
|
+
max_tier: "pro", # Max tier rarely needed; pro is sufficient for most tasks
|
|
1061
1061
|
allow_provider_switch: true,
|
|
1062
1062
|
auto_escalate: true,
|
|
1063
1063
|
escalation_threshold: 2,
|
|
@@ -1394,9 +1394,9 @@ module Aidp
|
|
|
1394
1394
|
|
|
1395
1395
|
# Execute a prompt with a specific provider
|
|
1396
1396
|
def execute_with_provider(provider_type, prompt, options = {})
|
|
1397
|
-
# Extract model
|
|
1397
|
+
# Extract model from options if provided
|
|
1398
1398
|
model_name = options.delete(:model)
|
|
1399
|
-
|
|
1399
|
+
retry_on_rate_limit = options.delete(:retry_on_rate_limit) != false # Default true
|
|
1400
1400
|
|
|
1401
1401
|
# Create provider factory instance
|
|
1402
1402
|
provider_factory = ProviderFactory.new
|
|
@@ -1415,11 +1415,10 @@ module Aidp
|
|
|
1415
1415
|
Aidp.logger.debug("provider_manager", "Executing with provider",
|
|
1416
1416
|
provider: provider_type,
|
|
1417
1417
|
model: model_name,
|
|
1418
|
-
tier: tier,
|
|
1419
1418
|
prompt_length: prompt.length)
|
|
1420
1419
|
|
|
1421
|
-
# Execute the prompt with the provider
|
|
1422
|
-
result = provider.send_message(prompt: prompt, session: nil
|
|
1420
|
+
# Execute the prompt with the provider
|
|
1421
|
+
result = provider.send_message(prompt: prompt, session: nil)
|
|
1423
1422
|
|
|
1424
1423
|
# Return structured result
|
|
1425
1424
|
{
|
|
@@ -1436,6 +1435,38 @@ module Aidp
|
|
|
1436
1435
|
}
|
|
1437
1436
|
rescue => e
|
|
1438
1437
|
log_rescue(e, component: "provider_manager", action: "execute_with_provider", fallback: "error_result", provider: provider_type, model: model_name, prompt_length: prompt.length)
|
|
1438
|
+
|
|
1439
|
+
# Detect rate limit / quota errors and attempt fallback
|
|
1440
|
+
error_message = e.message.to_s.downcase
|
|
1441
|
+
is_rate_limit = error_message.include?("rate limit") ||
|
|
1442
|
+
error_message.include?("quota") ||
|
|
1443
|
+
error_message.include?("limit reached") ||
|
|
1444
|
+
error_message.include?("resource exhausted") ||
|
|
1445
|
+
error_message.include?("too many requests")
|
|
1446
|
+
|
|
1447
|
+
if is_rate_limit && retry_on_rate_limit
|
|
1448
|
+
Aidp.logger.warn("provider_manager", "Rate limit detected, attempting fallback",
|
|
1449
|
+
provider: provider_type,
|
|
1450
|
+
model: model_name,
|
|
1451
|
+
error: e.message)
|
|
1452
|
+
|
|
1453
|
+
# Attempt to switch to fallback provider
|
|
1454
|
+
fallback_provider = switch_provider_for_error("rate_limit", {
|
|
1455
|
+
original_provider: provider_type,
|
|
1456
|
+
model: model_name,
|
|
1457
|
+
error_message: e.message
|
|
1458
|
+
})
|
|
1459
|
+
|
|
1460
|
+
if fallback_provider && fallback_provider != provider_type
|
|
1461
|
+
Aidp.logger.info("provider_manager", "Retrying with fallback provider",
|
|
1462
|
+
original: provider_type,
|
|
1463
|
+
fallback: fallback_provider)
|
|
1464
|
+
|
|
1465
|
+
# Retry with fallback provider (disable retry to prevent infinite loop)
|
|
1466
|
+
return execute_with_provider(fallback_provider, prompt, options.merge(retry_on_rate_limit: false))
|
|
1467
|
+
end
|
|
1468
|
+
end
|
|
1469
|
+
|
|
1439
1470
|
# Return error result
|
|
1440
1471
|
{
|
|
1441
1472
|
status: "error",
|
|
@@ -408,20 +408,10 @@ module Aidp
|
|
|
408
408
|
Aidp.log_debug("anthropic_provider", "error_classified",
|
|
409
409
|
exit_code: result.exit_status,
|
|
410
410
|
type: error_classification[:type],
|
|
411
|
-
confidence: error_classification[:confidence])
|
|
412
|
-
if error_classification[:is_rate_limit]
|
|
413
|
-
Aidp.log_debug("anthropic_provider", "rate_limit_detected",
|
|
414
|
-
exit_code: result.exit_status,
|
|
415
|
-
confidence: error_classification[:confidence],
|
|
416
|
-
message: combined)
|
|
417
|
-
notify_rate_limit(combined)
|
|
418
|
-
error_message = "Rate limit reached for Claude CLI.\n#{combined}"
|
|
419
|
-
error = RuntimeError.new(error_message)
|
|
420
|
-
debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
421
|
-
raise error
|
|
422
|
-
end
|
|
411
|
+
confidence: error_classification[:confidence])
|
|
423
412
|
|
|
424
|
-
# Check for model deprecation
|
|
413
|
+
# Check for model deprecation FIRST (before rate limiting)
|
|
414
|
+
# Even if rate limited, we need to cache the deprecation for next run
|
|
425
415
|
if error_classification[:is_deprecation]
|
|
426
416
|
deprecated_model = @model
|
|
427
417
|
Aidp.log_error("anthropic", "Model deprecation detected",
|
|
@@ -448,7 +438,7 @@ module Aidp
|
|
|
448
438
|
# Update model and retry
|
|
449
439
|
@model = replacement
|
|
450
440
|
|
|
451
|
-
# Retry with new model
|
|
441
|
+
# Retry with new model (even if rate limited, we'll hit rate limit with new model)
|
|
452
442
|
debug_log("🔄 Retrying with upgraded model: #{replacement}", level: :info)
|
|
453
443
|
return send_message(prompt: prompt, session: session, options: options)
|
|
454
444
|
else
|
|
@@ -469,6 +459,19 @@ module Aidp
|
|
|
469
459
|
end
|
|
470
460
|
end
|
|
471
461
|
|
|
462
|
+
# Check for rate limit (after handling deprecation)
|
|
463
|
+
if error_classification[:is_rate_limit]
|
|
464
|
+
Aidp.log_debug("anthropic_provider", "rate_limit_detected",
|
|
465
|
+
exit_code: result.exit_status,
|
|
466
|
+
confidence: error_classification[:confidence],
|
|
467
|
+
message: combined)
|
|
468
|
+
notify_rate_limit(combined)
|
|
469
|
+
error_message = "Rate limit reached for Claude CLI.\n#{combined}"
|
|
470
|
+
error = RuntimeError.new(error_message)
|
|
471
|
+
debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
472
|
+
raise error
|
|
473
|
+
end
|
|
474
|
+
|
|
472
475
|
# Check for auth issues
|
|
473
476
|
if combined.downcase.include?("oauth token has expired") || combined.downcase.include?("authentication_error")
|
|
474
477
|
error_message = "Authentication error from Claude CLI: token expired or invalid.\n" \
|
|
@@ -56,7 +56,13 @@ module Aidp
|
|
|
56
56
|
"api.cursor.sh",
|
|
57
57
|
"cursor.sh",
|
|
58
58
|
"app.cursor.sh",
|
|
59
|
-
"www.cursor.sh"
|
|
59
|
+
"www.cursor.sh",
|
|
60
|
+
# Authentication (Auth0)
|
|
61
|
+
"auth.cursor.sh",
|
|
62
|
+
"auth0.com",
|
|
63
|
+
"*.auth0.com",
|
|
64
|
+
"a0core.net",
|
|
65
|
+
"*.a0core.net"
|
|
60
66
|
],
|
|
61
67
|
ip_ranges: []
|
|
62
68
|
}
|
data/lib/aidp/setup/wizard.rb
CHANGED
|
@@ -50,6 +50,7 @@ module Aidp
|
|
|
50
50
|
return @saved if skip_wizard?
|
|
51
51
|
|
|
52
52
|
configure_providers
|
|
53
|
+
configure_harness_settings
|
|
53
54
|
configure_thinking_tiers
|
|
54
55
|
configure_work_loop
|
|
55
56
|
configure_branching
|
|
@@ -285,6 +286,24 @@ module Aidp
|
|
|
285
286
|
show_provider_summary(provider_choice, cleaned_fallbacks) unless provider_choice == "custom"
|
|
286
287
|
end
|
|
287
288
|
|
|
289
|
+
# -------------------------------------------
|
|
290
|
+
# Harness settings (retries, limits, etc.)
|
|
291
|
+
# -------------------------------------------
|
|
292
|
+
def configure_harness_settings
|
|
293
|
+
prompt.say("\n⚙️ Harness Configuration")
|
|
294
|
+
prompt.say(" Advanced settings for provider behavior")
|
|
295
|
+
existing = get([:harness]) || {}
|
|
296
|
+
|
|
297
|
+
return unless prompt.yes?("Configure advanced harness settings?", default: false)
|
|
298
|
+
|
|
299
|
+
max_retries = ask_with_default(
|
|
300
|
+
"Maximum retry attempts for failed LLM calls",
|
|
301
|
+
(existing[:max_retries] || 2).to_s
|
|
302
|
+
) { |value| value.to_i }
|
|
303
|
+
|
|
304
|
+
set([:harness, :max_retries], max_retries)
|
|
305
|
+
end
|
|
306
|
+
|
|
288
307
|
# Removed MCP configuration step (MCP now expected to be provider-specific if used)
|
|
289
308
|
|
|
290
309
|
# -------------------------------------------
|
|
@@ -473,6 +492,7 @@ module Aidp
|
|
|
473
492
|
prompt.say("\n⚙️ Work loop configuration")
|
|
474
493
|
prompt.say("-" * 40)
|
|
475
494
|
|
|
495
|
+
configure_work_loop_limits
|
|
476
496
|
configure_test_commands
|
|
477
497
|
configure_linting
|
|
478
498
|
configure_watch_patterns
|
|
@@ -482,6 +502,19 @@ module Aidp
|
|
|
482
502
|
configure_vcs_behavior
|
|
483
503
|
end
|
|
484
504
|
|
|
505
|
+
def configure_work_loop_limits
|
|
506
|
+
existing = get([:work_loop]) || {}
|
|
507
|
+
|
|
508
|
+
return unless prompt.yes?("Configure work loop limits?", default: false)
|
|
509
|
+
|
|
510
|
+
max_iterations = ask_with_default(
|
|
511
|
+
"Maximum work loop iterations",
|
|
512
|
+
(existing[:max_iterations] || 50).to_s
|
|
513
|
+
) { |value| value.to_i }
|
|
514
|
+
|
|
515
|
+
set([:work_loop, :max_iterations], max_iterations)
|
|
516
|
+
end
|
|
517
|
+
|
|
485
518
|
def configure_test_commands
|
|
486
519
|
existing = get([:work_loop, :test]) || {}
|
|
487
520
|
|
|
@@ -999,6 +1032,7 @@ module Aidp
|
|
|
999
1032
|
|
|
1000
1033
|
configure_watch_safety
|
|
1001
1034
|
configure_watch_labels
|
|
1035
|
+
configure_watch_change_requests
|
|
1002
1036
|
configure_watch_label_creation
|
|
1003
1037
|
end
|
|
1004
1038
|
|
|
@@ -1066,6 +1100,11 @@ module Aidp
|
|
|
1066
1100
|
existing[:ci_fix_trigger] || "aidp-fix-ci"
|
|
1067
1101
|
)
|
|
1068
1102
|
|
|
1103
|
+
auto_trigger = ask_with_default(
|
|
1104
|
+
"Label to trigger fully autonomous build+review+CI",
|
|
1105
|
+
existing[:auto_trigger] || "aidp-auto"
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1069
1108
|
change_request_trigger = ask_with_default(
|
|
1070
1109
|
"Label to trigger PR change implementation",
|
|
1071
1110
|
existing[:change_request_trigger] || "aidp-request-changes"
|
|
@@ -1078,10 +1117,35 @@ module Aidp
|
|
|
1078
1117
|
build_trigger: build_trigger,
|
|
1079
1118
|
review_trigger: review_trigger,
|
|
1080
1119
|
ci_fix_trigger: ci_fix_trigger,
|
|
1120
|
+
auto_trigger: auto_trigger,
|
|
1081
1121
|
change_request_trigger: change_request_trigger
|
|
1082
1122
|
})
|
|
1083
1123
|
end
|
|
1084
1124
|
|
|
1125
|
+
def configure_watch_change_requests
|
|
1126
|
+
prompt.say("\n📝 PR Change Request Configuration")
|
|
1127
|
+
prompt.say(" Configure how AIDP handles automated PR change requests")
|
|
1128
|
+
existing = get([:watch, :change_requests]) || {}
|
|
1129
|
+
|
|
1130
|
+
max_diff_size = ask_with_default(
|
|
1131
|
+
"Maximum PR diff size (lines) for change requests",
|
|
1132
|
+
(existing[:max_diff_size] || 5000).to_s
|
|
1133
|
+
) { |value| value.to_i }
|
|
1134
|
+
|
|
1135
|
+
post_comments = prompt.yes?(
|
|
1136
|
+
"Post detection comments when work is detected?",
|
|
1137
|
+
default: existing.fetch(:post_detection_comments, true)
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
set([:watch, :change_requests], {
|
|
1141
|
+
max_diff_size: max_diff_size
|
|
1142
|
+
})
|
|
1143
|
+
|
|
1144
|
+
set([:watch], {
|
|
1145
|
+
post_detection_comments: post_comments
|
|
1146
|
+
}.merge(get([:watch]) || {}))
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1085
1149
|
def configure_watch_label_creation
|
|
1086
1150
|
prompt.say("\n🏷️ GitHub Label Auto-Creation")
|
|
1087
1151
|
prompt.say(" Automatically create GitHub labels for watch mode if they don't exist")
|
|
@@ -1222,6 +1286,7 @@ module Aidp
|
|
|
1222
1286
|
build_trigger: "5319E7", # Purple
|
|
1223
1287
|
review_trigger: "FBCA04", # Yellow
|
|
1224
1288
|
ci_fix_trigger: "D93F0B", # Red
|
|
1289
|
+
auto_trigger: "0C8BD6", # Blue (distinct from build)
|
|
1225
1290
|
change_request_trigger: "F9D0C4", # Light pink
|
|
1226
1291
|
in_progress: "1D76DB" # Dark blue (internal coordination)
|
|
1227
1292
|
}
|
data/lib/aidp/version.rb
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../message_display"
|
|
4
|
+
require_relative "github_state_extractor"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Watch
|
|
8
|
+
# Handles the aidp-auto label on PRs by chaining review and CI-fix flows
|
|
9
|
+
# until the PR is ready for human review.
|
|
10
|
+
class AutoPrProcessor
|
|
11
|
+
include Aidp::MessageDisplay
|
|
12
|
+
|
|
13
|
+
DEFAULT_AUTO_LABEL = "aidp-auto"
|
|
14
|
+
|
|
15
|
+
def initialize(repository_client:, state_store:, review_processor:, ci_fix_processor:, label_config: {}, verbose: false)
|
|
16
|
+
@repository_client = repository_client
|
|
17
|
+
@state_store = state_store
|
|
18
|
+
@review_processor = review_processor
|
|
19
|
+
@ci_fix_processor = ci_fix_processor
|
|
20
|
+
@state_extractor = GitHubStateExtractor.new(repository_client: repository_client)
|
|
21
|
+
@verbose = verbose
|
|
22
|
+
@auto_label = label_config[:auto_trigger] || label_config["auto_trigger"] || DEFAULT_AUTO_LABEL
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def process(pr)
|
|
26
|
+
number = pr[:number]
|
|
27
|
+
Aidp.log_debug("auto_pr_processor", "process_started", pr: number, title: pr[:title])
|
|
28
|
+
display_message("🤖 Running autonomous review/CI loop for PR ##{number}", type: :info)
|
|
29
|
+
|
|
30
|
+
# Run review and CI fix flows. Each processor is responsible for its own guards.
|
|
31
|
+
@review_processor.process(pr)
|
|
32
|
+
@ci_fix_processor.process(pr)
|
|
33
|
+
|
|
34
|
+
finalize_if_ready(pr_number: number)
|
|
35
|
+
rescue => e
|
|
36
|
+
Aidp.log_error("auto_pr_processor", "process_failed", pr: pr[:number], error: e.message, error_class: e.class.name)
|
|
37
|
+
display_message("❌ aidp-auto failed on PR ##{pr[:number]}: #{e.message}", type: :error)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
attr_reader :auto_label
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def finalize_if_ready(pr_number:)
|
|
45
|
+
pr_data = @repository_client.fetch_pull_request(pr_number)
|
|
46
|
+
ci_status = @repository_client.fetch_ci_status(pr_number)
|
|
47
|
+
|
|
48
|
+
review_done = @state_extractor.review_completed?(pr_data) || @state_store.review_processed?(pr_number)
|
|
49
|
+
ci_passing = ci_status[:state] == "success"
|
|
50
|
+
|
|
51
|
+
Aidp.log_debug("auto_pr_processor", "completion_check",
|
|
52
|
+
pr: pr_number,
|
|
53
|
+
review_done: review_done,
|
|
54
|
+
ci_state: ci_status[:state])
|
|
55
|
+
|
|
56
|
+
return unless review_done && ci_passing
|
|
57
|
+
|
|
58
|
+
post_completion_comment(pr_number)
|
|
59
|
+
remove_auto_label(pr_number)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def post_completion_comment(pr_number)
|
|
63
|
+
comment = <<~COMMENT
|
|
64
|
+
## 🤖 aidp-auto
|
|
65
|
+
|
|
66
|
+
- Automated review completed
|
|
67
|
+
- CI is passing
|
|
68
|
+
|
|
69
|
+
Marking this PR ready for human review and removing the `#{@auto_label}` label.
|
|
70
|
+
COMMENT
|
|
71
|
+
|
|
72
|
+
@repository_client.post_comment(pr_number, comment)
|
|
73
|
+
display_message("💬 Posted aidp-auto completion comment on PR ##{pr_number}", type: :success)
|
|
74
|
+
rescue => e
|
|
75
|
+
Aidp.log_warn("auto_pr_processor", "comment_failed", pr: pr_number, error: e.message)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def remove_auto_label(pr_number)
|
|
79
|
+
@repository_client.remove_labels(pr_number, @auto_label)
|
|
80
|
+
display_message("🏷️ Removed '#{@auto_label}' from PR ##{pr_number}", type: :info)
|
|
81
|
+
rescue => e
|
|
82
|
+
Aidp.log_warn("auto_pr_processor", "remove_label_failed", pr: pr_number, error: e.message)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../message_display"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Watch
|
|
7
|
+
# Handles the aidp-auto label on issues by delegating to the BuildProcessor
|
|
8
|
+
# and transferring the label to the created PR once work completes.
|
|
9
|
+
class AutoProcessor
|
|
10
|
+
include Aidp::MessageDisplay
|
|
11
|
+
|
|
12
|
+
DEFAULT_AUTO_LABEL = "aidp-auto"
|
|
13
|
+
|
|
14
|
+
attr_reader :auto_label
|
|
15
|
+
|
|
16
|
+
def initialize(repository_client:, state_store:, build_processor:, label_config: {}, verbose: false)
|
|
17
|
+
@repository_client = repository_client
|
|
18
|
+
@state_store = state_store
|
|
19
|
+
@build_processor = build_processor
|
|
20
|
+
@verbose = verbose
|
|
21
|
+
|
|
22
|
+
# Allow overrides from watch config
|
|
23
|
+
@auto_label = label_config[:auto_trigger] || label_config["auto_trigger"] || DEFAULT_AUTO_LABEL
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def process(issue)
|
|
27
|
+
number = issue[:number]
|
|
28
|
+
Aidp.log_debug("auto_processor", "process_started", issue: number, title: issue[:title])
|
|
29
|
+
display_message("🤖 Starting autonomous build for issue ##{number}", type: :info)
|
|
30
|
+
|
|
31
|
+
@build_processor.process(issue)
|
|
32
|
+
|
|
33
|
+
status = @state_store.build_status(number)
|
|
34
|
+
pr_url = status["pr_url"]
|
|
35
|
+
pr_number = extract_pr_number(pr_url)
|
|
36
|
+
|
|
37
|
+
unless status["status"] == "completed" && pr_number
|
|
38
|
+
Aidp.log_debug("auto_processor", "no_pr_to_transfer", issue: number, status: status["status"], pr_url: pr_url)
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
transfer_label_to_pr(issue_number: number, pr_number: pr_number)
|
|
43
|
+
rescue => e
|
|
44
|
+
Aidp.log_error("auto_processor", "process_failed", issue: issue[:number], error: e.message, error_class: e.class.name)
|
|
45
|
+
display_message("❌ aidp-auto failed for issue ##{issue[:number]}: #{e.message}", type: :error)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def extract_pr_number(pr_url)
|
|
51
|
+
return nil unless pr_url
|
|
52
|
+
|
|
53
|
+
match = pr_url.match(%r{/pull/(\d+)}i)
|
|
54
|
+
match && match[1].to_i
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def transfer_label_to_pr(issue_number:, pr_number:)
|
|
58
|
+
Aidp.log_debug("auto_processor", "transferring_label", issue: issue_number, pr: pr_number, label: @auto_label)
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
@repository_client.add_labels(pr_number, @auto_label)
|
|
62
|
+
display_message("🏷️ Added '#{@auto_label}' to PR ##{pr_number}", type: :info)
|
|
63
|
+
rescue => e
|
|
64
|
+
Aidp.log_warn("auto_processor", "add_label_failed", pr: pr_number, label: @auto_label, error: e.message)
|
|
65
|
+
display_message("⚠️ Failed to add '#{@auto_label}' to PR ##{pr_number}: #{e.message}", type: :warn)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
@repository_client.remove_labels(issue_number, @auto_label)
|
|
70
|
+
display_message("🏷️ Removed '#{@auto_label}' from issue ##{issue_number}", type: :muted)
|
|
71
|
+
rescue => e
|
|
72
|
+
Aidp.log_warn("auto_processor", "remove_label_failed", issue: issue_number, label: @auto_label, error: e.message)
|
|
73
|
+
display_message("⚠️ Failed to remove '#{@auto_label}' from issue ##{issue_number}: #{e.message}", type: :warn)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -57,7 +57,8 @@ module Aidp
|
|
|
57
57
|
run_tests_before_push: true,
|
|
58
58
|
commit_message_prefix: "aidp: pr-change",
|
|
59
59
|
require_comment_reference: true,
|
|
60
|
-
max_diff_size: 2000
|
|
60
|
+
max_diff_size: 2000,
|
|
61
|
+
allow_large_pr_worktree_bypass: true # Default to always using worktree for large PRs
|
|
61
62
|
}.merge(symbolize_keys(change_request_config))
|
|
62
63
|
|
|
63
64
|
# Load safety configuration
|
|
@@ -95,16 +96,23 @@ module Aidp
|
|
|
95
96
|
return
|
|
96
97
|
end
|
|
97
98
|
|
|
98
|
-
#
|
|
99
|
+
# If max_diff_size is set, attempt to fetch and check diff
|
|
100
|
+
# But bypass restriction for worktree-based workflows
|
|
99
101
|
diff = @repository_client.fetch_pull_request_diff(number)
|
|
100
102
|
diff_size = diff.lines.count
|
|
101
103
|
|
|
102
|
-
if
|
|
104
|
+
# Check if we want to use the worktree bypass
|
|
105
|
+
use_worktree_bypass = @config[:allow_large_pr_worktree_bypass] || @config[:allow_large_pr_worktree_bypass].nil?
|
|
106
|
+
|
|
107
|
+
if diff_size > @config[:max_diff_size] && !use_worktree_bypass
|
|
103
108
|
display_message("⚠️ PR ##{number} diff too large (#{diff_size} lines > #{@config[:max_diff_size]}). Skipping.", type: :warn)
|
|
104
109
|
post_diff_too_large_comment(pr, diff_size)
|
|
105
110
|
return
|
|
106
111
|
end
|
|
107
112
|
|
|
113
|
+
# Log the diff size for observability
|
|
114
|
+
Aidp.log_debug("change_request_processor", "PR diff size", number: number, size: diff_size, max_allowed: @config[:max_diff_size], worktree_bypass: use_worktree_bypass)
|
|
115
|
+
|
|
108
116
|
# Analyze change requests
|
|
109
117
|
analysis_result = analyze_change_requests(pr_data: pr_data, comments: authorized_comments, diff: diff)
|
|
110
118
|
|
|
@@ -308,40 +316,105 @@ module Aidp
|
|
|
308
316
|
head_ref = pr_data[:head_ref]
|
|
309
317
|
pr_number = pr_data[:number]
|
|
310
318
|
|
|
311
|
-
|
|
319
|
+
worktree_path = resolve_worktree_for_pr(pr_data)
|
|
320
|
+
|
|
321
|
+
Dir.chdir(worktree_path) do
|
|
322
|
+
run_git(%w[fetch origin], allow_failure: true)
|
|
323
|
+
run_git(["checkout", head_ref])
|
|
324
|
+
run_git(%w[pull --ff-only], allow_failure: true)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
@project_dir = worktree_path
|
|
328
|
+
|
|
329
|
+
Aidp.log_debug("change_request_processor", "Checked out PR branch", branch: head_ref, worktree: worktree_path)
|
|
330
|
+
display_message("🌿 Using worktree for PR ##{pr_number}: #{head_ref}", type: :info)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def resolve_worktree_for_pr(pr_data)
|
|
334
|
+
head_ref = pr_data[:head_ref]
|
|
335
|
+
pr_number = pr_data[:number]
|
|
336
|
+
|
|
312
337
|
existing = Aidp::Worktree.find_by_branch(branch: head_ref, project_dir: @project_dir)
|
|
313
338
|
|
|
314
339
|
if existing && existing[:active]
|
|
315
340
|
display_message("🔄 Using existing worktree for branch: #{head_ref}", type: :info)
|
|
316
341
|
Aidp.log_debug("change_request_processor", "worktree_reused", pr_number: pr_number, branch: head_ref, path: existing[:path])
|
|
342
|
+
return existing[:path]
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
issue_worktree = find_issue_worktree_for_pr(pr_data)
|
|
346
|
+
return issue_worktree if issue_worktree
|
|
347
|
+
|
|
348
|
+
create_worktree_for_pr(pr_data)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def find_issue_worktree_for_pr(pr_data)
|
|
352
|
+
pr_number = pr_data[:number]
|
|
353
|
+
linked_issue_numbers = extract_issue_numbers_from_pr(pr_data)
|
|
354
|
+
|
|
355
|
+
build_match = @state_store.find_build_by_pr(pr_number)
|
|
356
|
+
linked_issue_numbers << build_match[:issue_number] if build_match
|
|
357
|
+
linked_issue_numbers = linked_issue_numbers.compact.uniq
|
|
317
358
|
|
|
318
|
-
|
|
319
|
-
|
|
359
|
+
linked_issue_numbers.each do |issue_number|
|
|
360
|
+
workstream = @state_store.workstream_for_issue(issue_number)
|
|
361
|
+
next unless workstream
|
|
320
362
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
363
|
+
slug = workstream[:workstream]
|
|
364
|
+
branch = workstream[:branch]
|
|
365
|
+
|
|
366
|
+
if slug
|
|
367
|
+
info = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
|
|
368
|
+
if info && info[:active]
|
|
369
|
+
Aidp.log_debug("change_request_processor", "issue_worktree_reused", pr_number: pr_number, issue_number: issue_number, branch: branch, path: info[:path])
|
|
370
|
+
display_message("🔄 Reusing worktree #{slug} for issue ##{issue_number} (PR ##{pr_number})", type: :info)
|
|
371
|
+
return info[:path]
|
|
372
|
+
end
|
|
326
373
|
end
|
|
327
374
|
|
|
328
|
-
|
|
375
|
+
if branch
|
|
376
|
+
existing = Aidp::Worktree.find_by_branch(branch: branch, project_dir: @project_dir)
|
|
377
|
+
if existing && existing[:active]
|
|
378
|
+
Aidp.log_debug("change_request_processor", "issue_branch_worktree_reused", pr_number: pr_number, issue_number: issue_number, branch: branch, path: existing[:path])
|
|
379
|
+
display_message("🔄 Reusing branch worktree for issue ##{issue_number}: #{branch}", type: :info)
|
|
380
|
+
return existing[:path]
|
|
381
|
+
end
|
|
382
|
+
end
|
|
329
383
|
end
|
|
330
384
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
# Fetch latest
|
|
334
|
-
run_git(%w[fetch origin])
|
|
385
|
+
nil
|
|
386
|
+
end
|
|
335
387
|
|
|
336
|
-
|
|
337
|
-
|
|
388
|
+
def extract_issue_numbers_from_pr(pr_data)
|
|
389
|
+
body = pr_data[:body].to_s
|
|
390
|
+
issue_matches = body.scan(/(?:Fixes|Resolves|Closes)\s+#(\d+)/i).flatten
|
|
338
391
|
|
|
339
|
-
|
|
340
|
-
|
|
392
|
+
issue_matches.map { |num| num.to_i }.uniq
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def create_worktree_for_pr(pr_data)
|
|
396
|
+
head_ref = pr_data[:head_ref]
|
|
397
|
+
pr_number = pr_data[:number]
|
|
398
|
+
slug = "pr-#{pr_number}-change-requests"
|
|
399
|
+
|
|
400
|
+
display_message("🌿 Creating worktree for PR ##{pr_number}: #{head_ref}", type: :info)
|
|
401
|
+
|
|
402
|
+
Dir.chdir(@project_dir) do
|
|
403
|
+
run_git(%w[fetch origin], allow_failure: true)
|
|
341
404
|
end
|
|
342
405
|
|
|
343
|
-
Aidp.
|
|
344
|
-
|
|
406
|
+
result = Aidp::Worktree.create(
|
|
407
|
+
slug: slug,
|
|
408
|
+
project_dir: @project_dir,
|
|
409
|
+
branch: head_ref,
|
|
410
|
+
base_branch: pr_data[:base_ref]
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
worktree_path = result[:path]
|
|
414
|
+
Aidp.log_debug("change_request_processor", "worktree_created", pr_number: pr_number, branch: head_ref, path: worktree_path)
|
|
415
|
+
display_message("✅ Worktree created at #{worktree_path}", type: :success)
|
|
416
|
+
|
|
417
|
+
worktree_path
|
|
345
418
|
end
|
|
346
419
|
|
|
347
420
|
def apply_changes(changes)
|
|
@@ -704,15 +777,20 @@ module Aidp
|
|
|
704
777
|
comment = <<~COMMENT
|
|
705
778
|
#{COMMENT_HEADER}
|
|
706
779
|
|
|
707
|
-
⚠️ PR diff is too large for
|
|
780
|
+
⚠️ PR diff is too large for default change requests.
|
|
708
781
|
|
|
709
782
|
**Current size:** #{diff_size} lines
|
|
710
783
|
**Maximum allowed:** #{@config[:max_diff_size]} lines
|
|
711
784
|
|
|
712
|
-
For large PRs,
|
|
713
|
-
1.
|
|
714
|
-
|
|
715
|
-
|
|
785
|
+
For large PRs, you have several options:
|
|
786
|
+
1. Enable worktree-based large PR handling:
|
|
787
|
+
Set `allow_large_pr_worktree_bypass: true` in your `aidp.yml`
|
|
788
|
+
2. Break the PR into smaller chunks
|
|
789
|
+
3. Implement changes manually
|
|
790
|
+
4. Increase `max_diff_size` in your configuration
|
|
791
|
+
|
|
792
|
+
The worktree bypass allows processing large PRs by working directly in the branch
|
|
793
|
+
instead of using diff-based changes.
|
|
716
794
|
COMMENT
|
|
717
795
|
|
|
718
796
|
begin
|
data/lib/aidp/watch/runner.rb
CHANGED
|
@@ -14,6 +14,8 @@ require_relative "../auto_update"
|
|
|
14
14
|
require_relative "review_processor"
|
|
15
15
|
require_relative "ci_fix_processor"
|
|
16
16
|
require_relative "change_request_processor"
|
|
17
|
+
require_relative "auto_processor"
|
|
18
|
+
require_relative "auto_pr_processor"
|
|
17
19
|
|
|
18
20
|
module Aidp
|
|
19
21
|
module Watch
|
|
@@ -70,6 +72,13 @@ module Aidp
|
|
|
70
72
|
verbose: verbose,
|
|
71
73
|
label_config: label_config
|
|
72
74
|
)
|
|
75
|
+
@auto_processor = AutoProcessor.new(
|
|
76
|
+
repository_client: @repository_client,
|
|
77
|
+
state_store: @state_store,
|
|
78
|
+
build_processor: @build_processor,
|
|
79
|
+
label_config: label_config,
|
|
80
|
+
verbose: verbose
|
|
81
|
+
)
|
|
73
82
|
|
|
74
83
|
# Initialize auto-update coordinator
|
|
75
84
|
@auto_update_coordinator = Aidp::AutoUpdate.coordinator(project_dir: project_dir)
|
|
@@ -90,6 +99,14 @@ module Aidp
|
|
|
90
99
|
label_config: label_config,
|
|
91
100
|
verbose: verbose
|
|
92
101
|
)
|
|
102
|
+
@auto_pr_processor = AutoPrProcessor.new(
|
|
103
|
+
repository_client: @repository_client,
|
|
104
|
+
state_store: @state_store,
|
|
105
|
+
review_processor: @review_processor,
|
|
106
|
+
ci_fix_processor: @ci_fix_processor,
|
|
107
|
+
label_config: label_config,
|
|
108
|
+
verbose: verbose
|
|
109
|
+
)
|
|
93
110
|
@change_request_processor = ChangeRequestProcessor.new(
|
|
94
111
|
repository_client: @repository_client,
|
|
95
112
|
state_store: @state_store,
|
|
@@ -142,9 +159,11 @@ module Aidp
|
|
|
142
159
|
def process_cycle
|
|
143
160
|
process_plan_triggers
|
|
144
161
|
process_build_triggers
|
|
162
|
+
process_auto_issue_triggers
|
|
145
163
|
check_for_updates_if_due
|
|
146
164
|
process_review_triggers
|
|
147
165
|
process_ci_fix_triggers
|
|
166
|
+
process_auto_pr_triggers
|
|
148
167
|
process_change_request_triggers
|
|
149
168
|
end
|
|
150
169
|
|
|
@@ -264,6 +283,54 @@ module Aidp
|
|
|
264
283
|
end
|
|
265
284
|
end
|
|
266
285
|
|
|
286
|
+
def process_auto_issue_triggers
|
|
287
|
+
auto_label = @auto_processor.auto_label
|
|
288
|
+
begin
|
|
289
|
+
issues = @repository_client.list_issues(labels: [auto_label], state: "open")
|
|
290
|
+
rescue => e
|
|
291
|
+
Aidp.log_error("watch_runner", "auto_issue_poll_failed", label: auto_label, error: e.message)
|
|
292
|
+
return
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
Aidp.log_debug("watch_runner", "auto_issue_poll", label: auto_label, total: issues.size)
|
|
296
|
+
|
|
297
|
+
issues.each do |issue|
|
|
298
|
+
unless issue_has_label?(issue, auto_label)
|
|
299
|
+
Aidp.log_debug("watch_runner", "auto_issue_skip_label_mismatch", issue: issue[:number], labels: issue[:labels])
|
|
300
|
+
next
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
begin
|
|
304
|
+
detailed = @repository_client.fetch_issue(issue[:number])
|
|
305
|
+
rescue => e
|
|
306
|
+
Aidp.log_error("watch_runner", "auto_issue_fetch_failed", issue: issue[:number], error: e.message)
|
|
307
|
+
next
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Check if already in progress by another instance
|
|
311
|
+
if @state_extractor.in_progress?(detailed)
|
|
312
|
+
Aidp.log_debug("watch_runner", "auto_issue_skip_in_progress", issue: detailed[:number])
|
|
313
|
+
next
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Check author authorization before processing
|
|
317
|
+
unless @safety_checker.should_process_issue?(detailed, enforce: false)
|
|
318
|
+
Aidp.log_debug("watch_runner", "auto_issue_skip_unauthorized_author", issue: detailed[:number], author: detailed[:author])
|
|
319
|
+
next
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Check if detection comment already posted (deduplication)
|
|
323
|
+
unless @state_extractor.detection_comment_posted?(detailed, auto_label)
|
|
324
|
+
post_detection_comment(item_type: :issue, number: detailed[:number], label: auto_label)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
Aidp.log_debug("watch_runner", "auto_issue_process", issue: detailed[:number])
|
|
328
|
+
@auto_processor.process(detailed)
|
|
329
|
+
rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
|
|
330
|
+
Aidp.log_warn("watch_runner", "unauthorized_issue_author_auto", issue: issue[:number], error: e.message)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
267
334
|
def process_review_triggers
|
|
268
335
|
review_label = @review_processor.review_label
|
|
269
336
|
begin
|
|
@@ -310,6 +377,43 @@ module Aidp
|
|
|
310
377
|
end
|
|
311
378
|
end
|
|
312
379
|
|
|
380
|
+
def process_auto_pr_triggers
|
|
381
|
+
auto_label = @auto_pr_processor.auto_label
|
|
382
|
+
prs = @repository_client.list_pull_requests(labels: [auto_label], state: "open")
|
|
383
|
+
Aidp.log_debug("watch_runner", "auto_pr_poll", label: auto_label, total: prs.size)
|
|
384
|
+
|
|
385
|
+
prs.each do |pr|
|
|
386
|
+
unless pr_has_label?(pr, auto_label)
|
|
387
|
+
Aidp.log_debug("watch_runner", "auto_pr_skip_label_mismatch", pr: pr[:number], labels: pr[:labels])
|
|
388
|
+
next
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
detailed = @repository_client.fetch_pull_request(pr[:number])
|
|
392
|
+
|
|
393
|
+
# Check if already in progress by another instance
|
|
394
|
+
if @state_extractor.in_progress?(detailed)
|
|
395
|
+
Aidp.log_debug("watch_runner", "auto_pr_skip_in_progress", pr: detailed[:number])
|
|
396
|
+
next
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Check author authorization before processing
|
|
400
|
+
unless @safety_checker.should_process_issue?(detailed, enforce: false)
|
|
401
|
+
Aidp.log_debug("watch_runner", "auto_pr_skip_unauthorized_author", pr: detailed[:number], author: detailed[:author])
|
|
402
|
+
next
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Check if detection comment already posted (deduplication)
|
|
406
|
+
unless @state_extractor.detection_comment_posted?(detailed, auto_label)
|
|
407
|
+
post_detection_comment(item_type: :pr, number: detailed[:number], label: auto_label)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
Aidp.log_debug("watch_runner", "auto_pr_process", pr: detailed[:number])
|
|
411
|
+
@auto_pr_processor.process(detailed)
|
|
412
|
+
rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
|
|
413
|
+
Aidp.log_warn("watch_runner", "unauthorized_pr_author_auto", pr: pr[:number], error: e.message)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
313
417
|
def process_ci_fix_triggers
|
|
314
418
|
ci_fix_label = @ci_fix_processor.ci_fix_label
|
|
315
419
|
prs = @repository_client.list_pull_requests(labels: [ci_fix_label], state: "open")
|
|
@@ -65,6 +65,43 @@ module Aidp
|
|
|
65
65
|
save!
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# Retrieve workstream metadata for a given issue
|
|
69
|
+
# @return [Hash, nil] {issue_number:, branch:, workstream:, pr_url:, status:}
|
|
70
|
+
def workstream_for_issue(issue_number)
|
|
71
|
+
data = build_status(issue_number)
|
|
72
|
+
return nil if data.nil? || data.empty?
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
issue_number: issue_number.to_i,
|
|
76
|
+
branch: data["branch"],
|
|
77
|
+
workstream: data["workstream"],
|
|
78
|
+
pr_url: data["pr_url"],
|
|
79
|
+
status: data["status"]
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Find the build/workstream metadata associated with a PR URL
|
|
84
|
+
# This is used to map change-request PRs back to their originating issues/worktrees.
|
|
85
|
+
# @return [Hash, nil] {issue_number:, branch:, workstream:, pr_url:, status:}
|
|
86
|
+
def find_build_by_pr(pr_number)
|
|
87
|
+
builds.each do |issue_number, data|
|
|
88
|
+
pr_url = data["pr_url"]
|
|
89
|
+
next unless pr_url
|
|
90
|
+
|
|
91
|
+
if pr_url.match?(%r{/pull/#{pr_number}\b})
|
|
92
|
+
return {
|
|
93
|
+
issue_number: issue_number.to_i,
|
|
94
|
+
branch: data["branch"],
|
|
95
|
+
workstream: data["workstream"],
|
|
96
|
+
pr_url: pr_url,
|
|
97
|
+
status: data["status"]
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
68
105
|
# Review tracking methods
|
|
69
106
|
def review_processed?(pr_number)
|
|
70
107
|
reviews.key?(pr_number.to_s)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
module Aidp
|
|
5
|
+
# Manages git worktrees for pull request branches
|
|
6
|
+
class WorktreeBranchManager
|
|
7
|
+
class WorktreeCreationError < StandardError; end
|
|
8
|
+
class WorktreeLookupError < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Initialize with a project directory and optional logger
|
|
11
|
+
def initialize(project_dir:, logger: Aidp.logger)
|
|
12
|
+
@project_dir = project_dir
|
|
13
|
+
@logger = logger
|
|
14
|
+
@worktree_registry_path = File.join(project_dir, ".aidp", "worktrees.json")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Find an existing worktree for a given branch or PR
|
|
18
|
+
def find_worktree(branch:)
|
|
19
|
+
Aidp.log_debug("worktree_branch_manager", "finding_worktree", branch: branch)
|
|
20
|
+
|
|
21
|
+
raise WorktreeLookupError, "Invalid git repository: #{@project_dir}" unless git_repository?
|
|
22
|
+
|
|
23
|
+
# Check registry first
|
|
24
|
+
worktree_info = read_registry.find { |w| w["branch"] == branch }
|
|
25
|
+
|
|
26
|
+
if worktree_info
|
|
27
|
+
worktree_path = worktree_info["path"]
|
|
28
|
+
return worktree_path if File.directory?(worktree_path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Fallback: Use git worktree list to find the worktree
|
|
32
|
+
worktree_list_output = run_git_command("git worktree list")
|
|
33
|
+
worktree_list_output.split("\n").each do |line|
|
|
34
|
+
path, branch_info = line.split(" ", 2)
|
|
35
|
+
return path if branch_info&.include?(branch)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
nil
|
|
39
|
+
rescue => e
|
|
40
|
+
Aidp.log_error("worktree_branch_manager", "worktree_lookup_failed",
|
|
41
|
+
error: e.message, branch: branch)
|
|
42
|
+
raise
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Create a new worktree for a branch
|
|
46
|
+
def create_worktree(branch:, base_branch: "main")
|
|
47
|
+
Aidp.log_debug("worktree_branch_manager", "creating_worktree",
|
|
48
|
+
branch: branch, base_branch: base_branch)
|
|
49
|
+
|
|
50
|
+
# Validate branch name to prevent path traversal
|
|
51
|
+
validate_branch_name!(branch)
|
|
52
|
+
|
|
53
|
+
# Check if worktree already exists
|
|
54
|
+
existing_worktree = find_worktree(branch: branch)
|
|
55
|
+
return existing_worktree if existing_worktree
|
|
56
|
+
|
|
57
|
+
# Ensure base branch exists
|
|
58
|
+
base_ref = (branch == "main") ? "main" : "refs/heads/#{base_branch}"
|
|
59
|
+
base_exists_cmd = "git show-ref --verify --quiet #{base_ref}"
|
|
60
|
+
|
|
61
|
+
system({"GIT_DIR" => File.join(@project_dir, ".git")}, "cd #{@project_dir} && #{base_exists_cmd}")
|
|
62
|
+
|
|
63
|
+
# If base branch doesn't exist locally, create it
|
|
64
|
+
unless $?.success?
|
|
65
|
+
system({"GIT_DIR" => File.join(@project_dir, ".git")}, "cd #{@project_dir} && git checkout -b #{base_branch}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Create worktree directory
|
|
69
|
+
worktree_name = branch.tr("/", "_")
|
|
70
|
+
worktree_path = File.join(@project_dir, ".worktrees", worktree_name)
|
|
71
|
+
|
|
72
|
+
# Ensure .worktrees directory exists
|
|
73
|
+
FileUtils.mkdir_p(File.join(@project_dir, ".worktrees"))
|
|
74
|
+
|
|
75
|
+
# Create the worktree
|
|
76
|
+
cmd = "git worktree add -b #{branch} #{worktree_path} #{base_branch}"
|
|
77
|
+
result = system({"GIT_DIR" => File.join(@project_dir, ".git")}, "cd #{@project_dir} && #{cmd}")
|
|
78
|
+
|
|
79
|
+
unless result
|
|
80
|
+
Aidp.log_error("worktree_branch_manager", "worktree_creation_failed",
|
|
81
|
+
branch: branch, base_branch: base_branch)
|
|
82
|
+
raise WorktreeCreationError, "Failed to create worktree for branch #{branch}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Update registry
|
|
86
|
+
update_registry(branch, worktree_path)
|
|
87
|
+
|
|
88
|
+
worktree_path
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def git_repository?
|
|
94
|
+
File.directory?(File.join(@project_dir, ".git"))
|
|
95
|
+
rescue
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def validate_branch_name!(branch)
|
|
100
|
+
if branch.include?("..") || branch.start_with?("/")
|
|
101
|
+
raise WorktreeCreationError, "Invalid branch name: #{branch}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def run_git_command(cmd)
|
|
106
|
+
Dir.chdir(@project_dir) do
|
|
107
|
+
output = `#{cmd} 2>&1`
|
|
108
|
+
raise StandardError, output unless $?.success?
|
|
109
|
+
output
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Read the worktree registry
|
|
114
|
+
def read_registry
|
|
115
|
+
return [] unless File.exist?(@worktree_registry_path)
|
|
116
|
+
|
|
117
|
+
begin
|
|
118
|
+
JSON.parse(File.read(@worktree_registry_path))
|
|
119
|
+
rescue JSON::ParserError
|
|
120
|
+
Aidp.log_warn("worktree_branch_manager", "invalid_registry",
|
|
121
|
+
path: @worktree_registry_path)
|
|
122
|
+
[]
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Update the worktree registry
|
|
127
|
+
def update_registry(branch, path)
|
|
128
|
+
# Ensure .aidp directory exists
|
|
129
|
+
FileUtils.mkdir_p(File.dirname(@worktree_registry_path))
|
|
130
|
+
|
|
131
|
+
registry = read_registry
|
|
132
|
+
|
|
133
|
+
# Remove existing entries for the same branch
|
|
134
|
+
registry.reject! { |w| w["branch"] == branch }
|
|
135
|
+
|
|
136
|
+
# Add new entry
|
|
137
|
+
registry << {
|
|
138
|
+
"branch" => branch,
|
|
139
|
+
"path" => path,
|
|
140
|
+
"created_at" => Time.now.to_i
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Write updated registry
|
|
144
|
+
File.write(@worktree_registry_path, JSON.pretty_generate(registry))
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aidp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.31.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -449,6 +449,8 @@ files:
|
|
|
449
449
|
- lib/aidp/utils/devcontainer_detector.rb
|
|
450
450
|
- lib/aidp/version.rb
|
|
451
451
|
- lib/aidp/watch.rb
|
|
452
|
+
- lib/aidp/watch/auto_pr_processor.rb
|
|
453
|
+
- lib/aidp/watch/auto_processor.rb
|
|
452
454
|
- lib/aidp/watch/build_processor.rb
|
|
453
455
|
- lib/aidp/watch/change_request_processor.rb
|
|
454
456
|
- lib/aidp/watch/ci_fix_processor.rb
|
|
@@ -473,6 +475,7 @@ files:
|
|
|
473
475
|
- lib/aidp/workstream_executor.rb
|
|
474
476
|
- lib/aidp/workstream_state.rb
|
|
475
477
|
- lib/aidp/worktree.rb
|
|
478
|
+
- lib/aidp/worktree_branch_manager.rb
|
|
476
479
|
- templates/COMMON/AGENT_BASE.md
|
|
477
480
|
- templates/COMMON/CONVENTIONS.md
|
|
478
481
|
- templates/COMMON/TEMPLATES/ADR_TEMPLATE.md
|