aidp 0.25.0 → 0.27.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/README.md +45 -6
- data/lib/aidp/analyze/error_handler.rb +11 -0
- data/lib/aidp/cli/checkpoint_command.rb +198 -0
- data/lib/aidp/cli/config_command.rb +71 -0
- data/lib/aidp/cli/enhanced_input.rb +2 -0
- data/lib/aidp/cli/first_run_wizard.rb +8 -7
- data/lib/aidp/cli/harness_command.rb +102 -0
- data/lib/aidp/cli/jobs_command.rb +3 -3
- data/lib/aidp/cli/mcp_dashboard.rb +4 -3
- data/lib/aidp/cli/models_command.rb +662 -0
- data/lib/aidp/cli/providers_command.rb +223 -0
- data/lib/aidp/cli.rb +35 -456
- data/lib/aidp/daemon/runner.rb +2 -2
- data/lib/aidp/debug_mixin.rb +2 -9
- data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
- data/lib/aidp/execute/checkpoint_display.rb +38 -37
- data/lib/aidp/execute/interactive_repl.rb +2 -1
- data/lib/aidp/execute/prompt_manager.rb +4 -4
- data/lib/aidp/execute/work_loop_runner.rb +253 -56
- data/lib/aidp/execute/workflow_selector.rb +2 -2
- data/lib/aidp/harness/config_loader.rb +20 -11
- data/lib/aidp/harness/config_manager.rb +5 -5
- data/lib/aidp/harness/config_schema.rb +30 -8
- data/lib/aidp/harness/configuration.rb +105 -4
- data/lib/aidp/harness/enhanced_runner.rb +24 -15
- data/lib/aidp/harness/error_handler.rb +26 -5
- data/lib/aidp/harness/filter_strategy.rb +45 -0
- data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
- data/lib/aidp/harness/model_cache.rb +269 -0
- data/lib/aidp/harness/model_discovery_service.rb +259 -0
- data/lib/aidp/harness/model_registry.rb +201 -0
- data/lib/aidp/harness/output_filter.rb +136 -0
- data/lib/aidp/harness/provider_manager.rb +18 -3
- data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
- data/lib/aidp/harness/runner.rb +5 -0
- data/lib/aidp/harness/test_runner.rb +165 -27
- data/lib/aidp/harness/thinking_depth_manager.rb +223 -7
- data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
- data/lib/aidp/logger.rb +35 -5
- data/lib/aidp/providers/adapter.rb +2 -4
- data/lib/aidp/providers/anthropic.rb +141 -128
- data/lib/aidp/providers/base.rb +98 -2
- data/lib/aidp/providers/capability_registry.rb +0 -1
- data/lib/aidp/providers/codex.rb +49 -67
- data/lib/aidp/providers/cursor.rb +71 -59
- data/lib/aidp/providers/gemini.rb +44 -60
- data/lib/aidp/providers/github_copilot.rb +2 -66
- data/lib/aidp/providers/kilocode.rb +24 -80
- data/lib/aidp/providers/opencode.rb +24 -80
- data/lib/aidp/safe_directory.rb +10 -3
- data/lib/aidp/setup/wizard.rb +345 -8
- data/lib/aidp/storage/csv_storage.rb +9 -3
- data/lib/aidp/storage/file_manager.rb +8 -2
- data/lib/aidp/storage/json_storage.rb +9 -3
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +40 -1
- data/lib/aidp/watch/change_request_processor.rb +659 -0
- data/lib/aidp/watch/plan_generator.rb +93 -14
- data/lib/aidp/watch/plan_processor.rb +71 -8
- data/lib/aidp/watch/repository_client.rb +85 -20
- data/lib/aidp/watch/review_processor.rb +3 -3
- data/lib/aidp/watch/runner.rb +37 -0
- data/lib/aidp/watch/state_store.rb +46 -1
- data/lib/aidp/workflows/guided_agent.rb +3 -3
- data/lib/aidp/workstream_executor.rb +5 -2
- data/lib/aidp.rb +4 -0
- data/templates/aidp-development.yml.example +2 -2
- data/templates/aidp-production.yml.example +3 -3
- data/templates/aidp.yml.example +53 -0
- metadata +14 -1
|
@@ -29,32 +29,103 @@ module Aidp
|
|
|
29
29
|
def initialize(provider_name: nil, verbose: false)
|
|
30
30
|
@provider_name = provider_name
|
|
31
31
|
@verbose = verbose
|
|
32
|
+
@providers_attempted = []
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def generate(issue)
|
|
35
|
-
provider
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
Aidp.log_debug("plan_generator", "generate.start", provider: @provider_name, issue: issue[:number])
|
|
37
|
+
|
|
38
|
+
# Try providers in fallback chain order
|
|
39
|
+
providers_to_try = build_provider_fallback_chain
|
|
40
|
+
Aidp.log_debug("plan_generator", "fallback_chain", providers: providers_to_try, count: providers_to_try.size)
|
|
41
|
+
|
|
42
|
+
providers_to_try.each do |provider_name|
|
|
43
|
+
next if @providers_attempted.include?(provider_name)
|
|
44
|
+
|
|
45
|
+
Aidp.log_debug("plan_generator", "trying_provider", provider: provider_name, attempted: @providers_attempted)
|
|
46
|
+
|
|
47
|
+
provider = resolve_provider(provider_name)
|
|
48
|
+
unless provider
|
|
49
|
+
Aidp.log_debug("plan_generator", "provider_unavailable", provider: provider_name, reason: "not resolved")
|
|
50
|
+
@providers_attempted << provider_name
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
Aidp.log_info("plan_generator", "generate_with_provider", provider: provider_name, issue: issue[:number])
|
|
56
|
+
result = generate_with_provider(provider, issue, provider_name)
|
|
57
|
+
if result
|
|
58
|
+
Aidp.log_info("plan_generator", "generation_success", provider: provider_name, issue: issue[:number])
|
|
59
|
+
return result
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Provider returned nil - try next provider
|
|
63
|
+
Aidp.log_warn("plan_generator", "provider_returned_nil", provider: provider_name)
|
|
64
|
+
@providers_attempted << provider_name
|
|
65
|
+
rescue => e
|
|
66
|
+
# Log error and try next provider in chain
|
|
67
|
+
Aidp.log_warn("plan_generator", "provider_failed", provider: provider_name, error: e.message, error_class: e.class.name)
|
|
68
|
+
@providers_attempted << provider_name
|
|
69
|
+
end
|
|
41
70
|
end
|
|
71
|
+
|
|
72
|
+
# All providers exhausted, fall back to heuristic
|
|
73
|
+
Aidp.log_warn("plan_generator", "all_providers_exhausted", attempted: @providers_attempted, falling_back: "heuristic")
|
|
74
|
+
display_message("⚠️ All providers unavailable or failed. Falling back to heuristic plan.", type: :warn)
|
|
75
|
+
heuristic_plan(issue)
|
|
42
76
|
rescue => e
|
|
43
|
-
|
|
77
|
+
Aidp.log_error("plan_generator", "generation_failed_unexpectedly", error: e.message, backtrace: e.backtrace&.first(3))
|
|
78
|
+
display_message("⚠️ Plan generation failed unexpectedly (#{e.message}). Using heuristic.", type: :warn)
|
|
44
79
|
heuristic_plan(issue)
|
|
45
80
|
end
|
|
46
81
|
|
|
47
82
|
private
|
|
48
83
|
|
|
49
|
-
def
|
|
50
|
-
|
|
84
|
+
def build_provider_fallback_chain
|
|
85
|
+
# Start with specified provider or default
|
|
86
|
+
primary_provider = @provider_name || detect_default_provider
|
|
87
|
+
providers = []
|
|
88
|
+
|
|
89
|
+
# Add primary provider first
|
|
90
|
+
providers << primary_provider if primary_provider
|
|
91
|
+
|
|
92
|
+
# Try to get fallback chain from config
|
|
93
|
+
begin
|
|
94
|
+
config_manager = Aidp::Harness::ConfigManager.new(Dir.pwd)
|
|
95
|
+
fallback_providers = config_manager.fallback_providers || []
|
|
96
|
+
|
|
97
|
+
# Add fallback providers that aren't already in the list
|
|
98
|
+
fallback_providers.each do |fallback|
|
|
99
|
+
providers << fallback unless providers.include?(fallback)
|
|
100
|
+
end
|
|
101
|
+
rescue => e
|
|
102
|
+
Aidp.log_debug("plan_generator", "config_fallback_unavailable", error: e.message)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# If we still have no providers, add cursor as last resort
|
|
106
|
+
providers << "cursor" if providers.empty?
|
|
107
|
+
|
|
108
|
+
# Remove duplicates while preserving order
|
|
109
|
+
providers.uniq
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def resolve_provider(provider_name = nil)
|
|
113
|
+
provider_name ||= @provider_name || detect_default_provider
|
|
51
114
|
return nil unless provider_name
|
|
52
115
|
|
|
116
|
+
Aidp.log_debug("plan_generator", "resolve_provider", provider: provider_name)
|
|
117
|
+
|
|
53
118
|
provider = Aidp::ProviderManager.get_provider(provider_name, use_harness: false)
|
|
54
|
-
return provider if provider&.available?
|
|
55
119
|
|
|
120
|
+
if provider&.available?
|
|
121
|
+
Aidp.log_debug("plan_generator", "provider_resolved", provider: provider_name, available: true)
|
|
122
|
+
return provider
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
Aidp.log_debug("plan_generator", "provider_not_available", provider: provider_name, available: provider&.available?)
|
|
56
126
|
nil
|
|
57
127
|
rescue => e
|
|
128
|
+
Aidp.log_warn("plan_generator", "resolve_provider_failed", provider: provider_name, error: e.message)
|
|
58
129
|
display_message("⚠️ Failed to resolve provider #{provider_name}: #{e.message}", type: :warn)
|
|
59
130
|
nil
|
|
60
131
|
end
|
|
@@ -66,9 +137,11 @@ module Aidp
|
|
|
66
137
|
"cursor"
|
|
67
138
|
end
|
|
68
139
|
|
|
69
|
-
def generate_with_provider(provider, issue)
|
|
140
|
+
def generate_with_provider(provider, issue, provider_name = "unknown")
|
|
70
141
|
payload = build_prompt(issue)
|
|
71
142
|
|
|
143
|
+
Aidp.log_debug("plan_generator", "sending_to_provider", provider: provider_name, prompt_length: payload.length)
|
|
144
|
+
|
|
72
145
|
if @verbose
|
|
73
146
|
display_message("\n--- Plan Generation Prompt ---", type: :muted)
|
|
74
147
|
display_message(payload.strip, type: :muted)
|
|
@@ -77,6 +150,8 @@ module Aidp
|
|
|
77
150
|
|
|
78
151
|
response = provider.send_message(prompt: payload)
|
|
79
152
|
|
|
153
|
+
Aidp.log_debug("plan_generator", "provider_response_received", provider: provider_name, response_length: response&.length || 0)
|
|
154
|
+
|
|
80
155
|
if @verbose
|
|
81
156
|
display_message("\n--- Provider Response ---", type: :muted)
|
|
82
157
|
display_message(response.strip, type: :muted)
|
|
@@ -85,10 +160,14 @@ module Aidp
|
|
|
85
160
|
|
|
86
161
|
parsed = parse_structured_response(response)
|
|
87
162
|
|
|
88
|
-
|
|
163
|
+
if parsed
|
|
164
|
+
Aidp.log_debug("plan_generator", "response_parsed", provider: provider_name, has_summary: !parsed[:summary].to_s.empty?, tasks_count: parsed[:tasks]&.size || 0)
|
|
165
|
+
return parsed
|
|
166
|
+
end
|
|
89
167
|
|
|
90
|
-
|
|
91
|
-
|
|
168
|
+
Aidp.log_warn("plan_generator", "parse_failed", provider: provider_name)
|
|
169
|
+
display_message("⚠️ Unable to parse #{provider_name} response. Trying next provider.", type: :warn)
|
|
170
|
+
nil
|
|
92
171
|
end
|
|
93
172
|
|
|
94
173
|
def build_prompt(issue)
|
|
@@ -41,21 +41,46 @@ module Aidp
|
|
|
41
41
|
|
|
42
42
|
def process(issue)
|
|
43
43
|
number = issue[:number]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
existing_plan = @state_store.plan_data(number)
|
|
45
|
+
|
|
46
|
+
if existing_plan
|
|
47
|
+
display_message("🔄 Re-planning for issue ##{number} (iteration #{@state_store.plan_iteration_count(number) + 1})", type: :info)
|
|
48
|
+
else
|
|
49
|
+
display_message("🧠 Generating plan for issue ##{number} (#{issue[:title]})", type: :info)
|
|
47
50
|
end
|
|
48
51
|
|
|
49
|
-
display_message("🧠 Generating plan for issue ##{number} (#{issue[:title]})", type: :info)
|
|
50
52
|
plan_data = @plan_generator.generate(issue)
|
|
51
53
|
|
|
52
54
|
# Fetch the user who added the most recent label
|
|
53
55
|
label_actor = @repository_client.most_recent_label_actor(number)
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
# If updating existing plan, archive the previous content
|
|
58
|
+
archived_content = existing_plan ? archive_previous_plan(number, existing_plan) : nil
|
|
59
|
+
|
|
60
|
+
comment_body = build_comment(issue: issue, plan: plan_data, label_actor: label_actor, archived_content: archived_content)
|
|
61
|
+
|
|
62
|
+
if existing_plan && existing_plan["comment_id"]
|
|
63
|
+
# Update existing comment
|
|
64
|
+
@repository_client.update_comment(existing_plan["comment_id"], comment_body)
|
|
65
|
+
display_message("📝 Updated plan comment for issue ##{number}", type: :success)
|
|
66
|
+
elsif existing_plan
|
|
67
|
+
# Try to find existing comment by header
|
|
68
|
+
existing_comment = @repository_client.find_comment(number, COMMENT_HEADER)
|
|
69
|
+
if existing_comment
|
|
70
|
+
@repository_client.update_comment(existing_comment[:id], comment_body)
|
|
71
|
+
display_message("📝 Updated plan comment for issue ##{number}", type: :success)
|
|
72
|
+
plan_data = plan_data.merge(comment_id: existing_comment[:id])
|
|
73
|
+
else
|
|
74
|
+
# Fallback to posting new comment if we can't find the old one
|
|
75
|
+
@repository_client.post_comment(number, comment_body)
|
|
76
|
+
display_message("💬 Posted new plan comment for issue ##{number}", type: :success)
|
|
77
|
+
end
|
|
78
|
+
else
|
|
79
|
+
# First time planning - post new comment
|
|
80
|
+
@repository_client.post_comment(number, comment_body)
|
|
81
|
+
display_message("💬 Posted plan comment for issue ##{number}", type: :success)
|
|
82
|
+
end
|
|
57
83
|
|
|
58
|
-
display_message("💬 Posted plan comment for issue ##{number}", type: :success)
|
|
59
84
|
@state_store.record_plan(number, plan_data.merge(comment_body: comment_body, comment_hint: COMMENT_HEADER))
|
|
60
85
|
|
|
61
86
|
# Update labels: remove plan trigger, add appropriate status label
|
|
@@ -64,6 +89,31 @@ module Aidp
|
|
|
64
89
|
|
|
65
90
|
private
|
|
66
91
|
|
|
92
|
+
def archive_previous_plan(number, existing_plan)
|
|
93
|
+
iteration = @state_store.plan_iteration_count(number)
|
|
94
|
+
timestamp = existing_plan["posted_at"] || "unknown"
|
|
95
|
+
|
|
96
|
+
archived_parts = []
|
|
97
|
+
archived_parts << "<!-- ARCHIVED_PLAN_START iteration=#{iteration} timestamp=#{timestamp} -->"
|
|
98
|
+
archived_parts << "<details>"
|
|
99
|
+
archived_parts << "<summary>📋 Previous Plan (Iteration #{iteration}) - #{timestamp}</summary>"
|
|
100
|
+
archived_parts << ""
|
|
101
|
+
archived_parts << "<!-- ARCHIVED_PLAN_SUMMARY_START -->"
|
|
102
|
+
archived_parts << "### Plan Summary"
|
|
103
|
+
archived_parts << existing_plan["summary"].to_s
|
|
104
|
+
archived_parts << "<!-- ARCHIVED_PLAN_SUMMARY_END -->"
|
|
105
|
+
archived_parts << ""
|
|
106
|
+
archived_parts << "<!-- ARCHIVED_PLAN_TASKS_START -->"
|
|
107
|
+
archived_parts << "### Proposed Tasks"
|
|
108
|
+
archived_parts << format_bullets(Array(existing_plan["tasks"]), placeholder: "_No tasks_")
|
|
109
|
+
archived_parts << "<!-- ARCHIVED_PLAN_TASKS_END -->"
|
|
110
|
+
archived_parts << ""
|
|
111
|
+
archived_parts << "</details>"
|
|
112
|
+
archived_parts << "<!-- ARCHIVED_PLAN_END -->"
|
|
113
|
+
|
|
114
|
+
archived_parts.join("\n")
|
|
115
|
+
end
|
|
116
|
+
|
|
67
117
|
def update_labels_after_plan(number, plan_data)
|
|
68
118
|
questions = Array(plan_data[:questions])
|
|
69
119
|
has_questions = questions.any? && !questions.all? { |q| q.to_s.strip.empty? }
|
|
@@ -85,7 +135,7 @@ module Aidp
|
|
|
85
135
|
end
|
|
86
136
|
end
|
|
87
137
|
|
|
88
|
-
def build_comment(issue:, plan:, label_actor: nil)
|
|
138
|
+
def build_comment(issue:, plan:, label_actor: nil, archived_content: nil)
|
|
89
139
|
summary = plan[:summary].to_s.strip
|
|
90
140
|
tasks = Array(plan[:tasks])
|
|
91
141
|
questions = Array(plan[:questions])
|
|
@@ -104,14 +154,27 @@ module Aidp
|
|
|
104
154
|
parts << "**Issue**: [##{issue[:number]}](#{issue[:url]})"
|
|
105
155
|
parts << "**Title**: #{issue[:title]}"
|
|
106
156
|
parts << ""
|
|
157
|
+
|
|
158
|
+
# Add archived content if this is a plan update
|
|
159
|
+
if archived_content
|
|
160
|
+
parts << archived_content
|
|
161
|
+
parts << ""
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
parts << "<!-- PLAN_SUMMARY_START -->"
|
|
107
165
|
parts << "### Plan Summary"
|
|
108
166
|
parts << (summary.empty? ? "_No summary generated_" : summary)
|
|
167
|
+
parts << "<!-- PLAN_SUMMARY_END -->"
|
|
109
168
|
parts << ""
|
|
169
|
+
parts << "<!-- PLAN_TASKS_START -->"
|
|
110
170
|
parts << "### Proposed Tasks"
|
|
111
171
|
parts << format_bullets(tasks, placeholder: "_Pending task breakdown_")
|
|
172
|
+
parts << "<!-- PLAN_TASKS_END -->"
|
|
112
173
|
parts << ""
|
|
174
|
+
parts << "<!-- CLARIFYING_QUESTIONS_START -->"
|
|
113
175
|
parts << "### Clarifying Questions"
|
|
114
176
|
parts << format_numbered(questions, placeholder: "_No questions identified_")
|
|
177
|
+
parts << "<!-- CLARIFYING_QUESTIONS_END -->"
|
|
115
178
|
parts << ""
|
|
116
179
|
|
|
117
180
|
# Add instructions based on whether there are questions
|
|
@@ -61,6 +61,14 @@ module Aidp
|
|
|
61
61
|
gh_available? ? post_comment_via_gh(number, body) : post_comment_via_api(number, body)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
def find_comment(number, header_text)
|
|
65
|
+
gh_available? ? find_comment_via_gh(number, header_text) : find_comment_via_api(number, header_text)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def update_comment(comment_id, body)
|
|
69
|
+
gh_available? ? update_comment_via_gh(comment_id, body) : update_comment_via_api(comment_id, body)
|
|
70
|
+
end
|
|
71
|
+
|
|
64
72
|
def create_pull_request(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil)
|
|
65
73
|
gh_available? ? create_pull_request_via_gh(title: title, body: body, head: head, base: base, issue_number: issue_number, draft: draft, assignee: assignee) : raise("GitHub CLI not available - cannot create PR")
|
|
66
74
|
end
|
|
@@ -108,6 +116,10 @@ module Aidp
|
|
|
108
116
|
gh_available? ? list_pull_requests_via_gh(labels: labels, state: state) : list_pull_requests_via_api(labels: labels, state: state)
|
|
109
117
|
end
|
|
110
118
|
|
|
119
|
+
def fetch_pr_comments(number)
|
|
120
|
+
gh_available? ? fetch_pr_comments_via_gh(number) : fetch_pr_comments_via_api(number)
|
|
121
|
+
end
|
|
122
|
+
|
|
111
123
|
private
|
|
112
124
|
|
|
113
125
|
def list_issues_via_gh(labels:, state:)
|
|
@@ -162,27 +174,11 @@ module Aidp
|
|
|
162
174
|
raise "GitHub API error (#{response.code})" unless response.code == "200"
|
|
163
175
|
|
|
164
176
|
data = JSON.parse(response.body)
|
|
165
|
-
comments =
|
|
177
|
+
comments = fetch_pr_comments_via_api(number)
|
|
166
178
|
data["comments"] = comments
|
|
167
179
|
normalize_issue_detail_api(data)
|
|
168
180
|
end
|
|
169
181
|
|
|
170
|
-
def fetch_comments_via_api(number)
|
|
171
|
-
uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/comments")
|
|
172
|
-
response = Net::HTTP.get_response(uri)
|
|
173
|
-
return [] unless response.code == "200"
|
|
174
|
-
|
|
175
|
-
JSON.parse(response.body).map do |raw|
|
|
176
|
-
{
|
|
177
|
-
"body" => raw["body"],
|
|
178
|
-
"author" => raw.dig("user", "login"),
|
|
179
|
-
"createdAt" => raw["created_at"]
|
|
180
|
-
}
|
|
181
|
-
end
|
|
182
|
-
rescue
|
|
183
|
-
[]
|
|
184
|
-
end
|
|
185
|
-
|
|
186
182
|
def post_comment_via_gh(number, body)
|
|
187
183
|
cmd = ["gh", "issue", "comment", number.to_s, "--repo", full_repo, "--body", body]
|
|
188
184
|
stdout, stderr, status = Open3.capture3(*cmd)
|
|
@@ -205,6 +201,44 @@ module Aidp
|
|
|
205
201
|
response.body
|
|
206
202
|
end
|
|
207
203
|
|
|
204
|
+
def find_comment_via_gh(number, header_text)
|
|
205
|
+
comments = fetch_pr_comments_via_gh(number)
|
|
206
|
+
comments.find { |comment| comment[:body]&.include?(header_text) }
|
|
207
|
+
rescue => e
|
|
208
|
+
Aidp.log_warn("repository_client", "Failed to find comment", error: e.message)
|
|
209
|
+
nil
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def find_comment_via_api(number, header_text)
|
|
213
|
+
comments = fetch_pr_comments_via_api(number)
|
|
214
|
+
comments.find { |comment| comment[:body]&.include?(header_text) }
|
|
215
|
+
rescue => e
|
|
216
|
+
Aidp.log_warn("repository_client", "Failed to find comment", error: e.message)
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def update_comment_via_gh(comment_id, body)
|
|
221
|
+
cmd = ["gh", "api", "repos/#{full_repo}/issues/comments/#{comment_id}", "-X", "PATCH", "-f", "body=#{body}"]
|
|
222
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
223
|
+
raise "Failed to update comment via gh: #{stderr.strip}" unless status.success?
|
|
224
|
+
|
|
225
|
+
stdout.strip
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def update_comment_via_api(comment_id, body)
|
|
229
|
+
uri = URI("https://api.github.com/repos/#{full_repo}/issues/comments/#{comment_id}")
|
|
230
|
+
request = Net::HTTP::Patch.new(uri)
|
|
231
|
+
request["Content-Type"] = "application/json"
|
|
232
|
+
request.body = JSON.dump({body: body})
|
|
233
|
+
|
|
234
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
235
|
+
http.request(request)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
raise "GitHub API update comment failed (#{response.code})" unless response.code.start_with?("2")
|
|
239
|
+
response.body
|
|
240
|
+
end
|
|
241
|
+
|
|
208
242
|
def create_pull_request_via_gh(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil)
|
|
209
243
|
cmd = [
|
|
210
244
|
"gh", "pr", "create",
|
|
@@ -535,6 +569,27 @@ module Aidp
|
|
|
535
569
|
response.body
|
|
536
570
|
end
|
|
537
571
|
|
|
572
|
+
def fetch_pr_comments_via_gh(number)
|
|
573
|
+
cmd = ["gh", "api", "repos/#{full_repo}/issues/#{number}/comments", "--jq", "."]
|
|
574
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
575
|
+
raise "Failed to fetch PR comments via gh: #{stderr.strip}" unless status.success?
|
|
576
|
+
|
|
577
|
+
JSON.parse(stdout).map { |raw| normalize_pr_comment(raw) }
|
|
578
|
+
rescue JSON::ParserError => e
|
|
579
|
+
raise "Failed to parse PR comments response: #{e.message}"
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def fetch_pr_comments_via_api(number)
|
|
583
|
+
uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/comments")
|
|
584
|
+
response = Net::HTTP.get_response(uri)
|
|
585
|
+
return [] unless response.code == "200"
|
|
586
|
+
|
|
587
|
+
JSON.parse(response.body).map { |raw| normalize_pr_comment(raw) }
|
|
588
|
+
rescue => e
|
|
589
|
+
Aidp.log_warn("repository_client", "Failed to fetch PR comments", error: e.message)
|
|
590
|
+
[]
|
|
591
|
+
end
|
|
592
|
+
|
|
538
593
|
# Normalization methods for PRs
|
|
539
594
|
def normalize_pull_request(raw)
|
|
540
595
|
{
|
|
@@ -691,14 +746,24 @@ module Aidp
|
|
|
691
746
|
def normalize_comment(comment)
|
|
692
747
|
if comment.is_a?(Hash)
|
|
693
748
|
{
|
|
694
|
-
"body" => comment["body"],
|
|
695
|
-
"author" => comment["author"] || comment.dig("user", "login"),
|
|
696
|
-
"createdAt" => comment["createdAt"] || comment["created_at"]
|
|
749
|
+
"body" => comment["body"] || comment[:body],
|
|
750
|
+
"author" => comment["author"] || comment[:author] || comment.dig("user", "login"),
|
|
751
|
+
"createdAt" => comment["createdAt"] || comment[:created_at] || comment["created_at"]
|
|
697
752
|
}
|
|
698
753
|
else
|
|
699
754
|
{"body" => comment.to_s}
|
|
700
755
|
end
|
|
701
756
|
end
|
|
757
|
+
|
|
758
|
+
def normalize_pr_comment(raw)
|
|
759
|
+
{
|
|
760
|
+
id: raw["id"],
|
|
761
|
+
body: raw["body"],
|
|
762
|
+
author: raw.dig("user", "login"),
|
|
763
|
+
created_at: raw["created_at"],
|
|
764
|
+
updated_at: raw["updated_at"]
|
|
765
|
+
}
|
|
766
|
+
end
|
|
702
767
|
end
|
|
703
768
|
end
|
|
704
769
|
end
|
|
@@ -23,7 +23,7 @@ module Aidp
|
|
|
23
23
|
|
|
24
24
|
attr_reader :review_label
|
|
25
25
|
|
|
26
|
-
def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, verbose: false)
|
|
26
|
+
def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, verbose: false, reviewers: nil)
|
|
27
27
|
@repository_client = repository_client
|
|
28
28
|
@state_store = state_store
|
|
29
29
|
@provider_name = provider_name
|
|
@@ -33,8 +33,8 @@ module Aidp
|
|
|
33
33
|
# Load label configuration
|
|
34
34
|
@review_label = label_config[:review_trigger] || label_config["review_trigger"] || DEFAULT_REVIEW_LABEL
|
|
35
35
|
|
|
36
|
-
# Initialize reviewers
|
|
37
|
-
@reviewers = [
|
|
36
|
+
# Initialize reviewers (allow dependency injection for testing)
|
|
37
|
+
@reviewers = reviewers || [
|
|
38
38
|
Reviewers::SeniorDevReviewer.new(provider_name: provider_name),
|
|
39
39
|
Reviewers::SecurityReviewer.new(provider_name: provider_name),
|
|
40
40
|
Reviewers::PerformanceReviewer.new(provider_name: provider_name)
|
data/lib/aidp/watch/runner.rb
CHANGED
|
@@ -12,6 +12,7 @@ require_relative "build_processor"
|
|
|
12
12
|
require_relative "../auto_update"
|
|
13
13
|
require_relative "review_processor"
|
|
14
14
|
require_relative "ci_fix_processor"
|
|
15
|
+
require_relative "change_request_processor"
|
|
15
16
|
|
|
16
17
|
module Aidp
|
|
17
18
|
module Watch
|
|
@@ -77,6 +78,16 @@ module Aidp
|
|
|
77
78
|
label_config: label_config,
|
|
78
79
|
verbose: verbose
|
|
79
80
|
)
|
|
81
|
+
@change_request_processor = ChangeRequestProcessor.new(
|
|
82
|
+
repository_client: @repository_client,
|
|
83
|
+
state_store: @state_store,
|
|
84
|
+
provider_name: provider_name,
|
|
85
|
+
project_dir: project_dir,
|
|
86
|
+
label_config: label_config,
|
|
87
|
+
change_request_config: safety_config[:pr_change_requests] || safety_config["pr_change_requests"] || {},
|
|
88
|
+
safety_config: safety_config[:safety] || safety_config["safety"] || {},
|
|
89
|
+
verbose: verbose
|
|
90
|
+
)
|
|
80
91
|
end
|
|
81
92
|
|
|
82
93
|
def start
|
|
@@ -122,6 +133,7 @@ module Aidp
|
|
|
122
133
|
check_for_updates_if_due
|
|
123
134
|
process_review_triggers
|
|
124
135
|
process_ci_fix_triggers
|
|
136
|
+
process_change_request_triggers
|
|
125
137
|
end
|
|
126
138
|
|
|
127
139
|
def process_plan_triggers
|
|
@@ -230,6 +242,31 @@ module Aidp
|
|
|
230
242
|
end
|
|
231
243
|
end
|
|
232
244
|
|
|
245
|
+
def process_change_request_triggers
|
|
246
|
+
change_request_label = @change_request_processor.change_request_label
|
|
247
|
+
prs = @repository_client.list_pull_requests(labels: [change_request_label], state: "open")
|
|
248
|
+
Aidp.log_debug("watch_runner", "change_request_poll", label: change_request_label, total: prs.size)
|
|
249
|
+
prs.each do |pr|
|
|
250
|
+
unless pr_has_label?(pr, change_request_label)
|
|
251
|
+
Aidp.log_debug("watch_runner", "change_request_skip_label_mismatch", pr: pr[:number], labels: pr[:labels])
|
|
252
|
+
next
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
detailed = @repository_client.fetch_pull_request(pr[:number])
|
|
256
|
+
|
|
257
|
+
# Check author authorization before processing
|
|
258
|
+
unless @safety_checker.should_process_issue?(detailed, enforce: false)
|
|
259
|
+
Aidp.log_debug("watch_runner", "change_request_skip_unauthorized_author", pr: detailed[:number], author: detailed[:author])
|
|
260
|
+
next
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
Aidp.log_debug("watch_runner", "change_request_process", pr: detailed[:number])
|
|
264
|
+
@change_request_processor.process(detailed)
|
|
265
|
+
rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
|
|
266
|
+
Aidp.log_warn("watch_runner", "unauthorized PR author", pr: pr[:number], error: e.message)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
233
270
|
def issue_has_label?(issue, label)
|
|
234
271
|
Array(issue[:labels]).any? do |issue_label|
|
|
235
272
|
name = issue_label.is_a?(Hash) ? issue_label["name"] : issue_label.to_s
|
|
@@ -27,14 +27,26 @@ module Aidp
|
|
|
27
27
|
plans[issue_number.to_s]
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def plan_iteration_count(issue_number)
|
|
31
|
+
plan = plans[issue_number.to_s]
|
|
32
|
+
return 0 unless plan
|
|
33
|
+
plan["iteration"] || 1
|
|
34
|
+
end
|
|
35
|
+
|
|
30
36
|
def record_plan(issue_number, data)
|
|
37
|
+
existing_plan = plans[issue_number.to_s]
|
|
38
|
+
iteration = existing_plan ? (existing_plan["iteration"] || 1) + 1 : 1
|
|
39
|
+
|
|
31
40
|
payload = {
|
|
32
41
|
"summary" => data[:summary],
|
|
33
42
|
"tasks" => data[:tasks],
|
|
34
43
|
"questions" => data[:questions],
|
|
35
44
|
"comment_body" => data[:comment_body],
|
|
36
45
|
"comment_hint" => data[:comment_hint],
|
|
37
|
-
"
|
|
46
|
+
"comment_id" => data[:comment_id],
|
|
47
|
+
"posted_at" => data[:posted_at] || Time.now.utc.iso8601,
|
|
48
|
+
"iteration" => iteration,
|
|
49
|
+
"previous_iteration_at" => existing_plan ? existing_plan["posted_at"] : nil
|
|
38
50
|
}.compact
|
|
39
51
|
|
|
40
52
|
plans[issue_number.to_s] = payload
|
|
@@ -96,6 +108,34 @@ module Aidp
|
|
|
96
108
|
save!
|
|
97
109
|
end
|
|
98
110
|
|
|
111
|
+
# Change request tracking methods
|
|
112
|
+
def change_request_processed?(pr_number)
|
|
113
|
+
change_requests.key?(pr_number.to_s)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def change_request_data(pr_number)
|
|
117
|
+
change_requests[pr_number.to_s]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def record_change_request(pr_number, data)
|
|
121
|
+
payload = {
|
|
122
|
+
"status" => data[:status],
|
|
123
|
+
"timestamp" => data[:timestamp] || Time.now.utc.iso8601,
|
|
124
|
+
"changes_applied" => data[:changes_applied],
|
|
125
|
+
"commits" => data[:commits],
|
|
126
|
+
"reason" => data[:reason],
|
|
127
|
+
"clarification_count" => data[:clarification_count]
|
|
128
|
+
}.compact
|
|
129
|
+
|
|
130
|
+
change_requests[pr_number.to_s] = payload
|
|
131
|
+
save!
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def reset_change_request_state(pr_number)
|
|
135
|
+
change_requests.delete(pr_number.to_s)
|
|
136
|
+
save!
|
|
137
|
+
end
|
|
138
|
+
|
|
99
139
|
private
|
|
100
140
|
|
|
101
141
|
def ensure_directory
|
|
@@ -126,6 +166,7 @@ module Aidp
|
|
|
126
166
|
base["builds"] ||= {}
|
|
127
167
|
base["reviews"] ||= {}
|
|
128
168
|
base["ci_fixes"] ||= {}
|
|
169
|
+
base["change_requests"] ||= {}
|
|
129
170
|
base
|
|
130
171
|
end
|
|
131
172
|
end
|
|
@@ -146,6 +187,10 @@ module Aidp
|
|
|
146
187
|
state["ci_fixes"]
|
|
147
188
|
end
|
|
148
189
|
|
|
190
|
+
def change_requests
|
|
191
|
+
state["change_requests"]
|
|
192
|
+
end
|
|
193
|
+
|
|
149
194
|
def stringify_keys(hash)
|
|
150
195
|
return {} unless hash
|
|
151
196
|
|
|
@@ -19,7 +19,7 @@ module Aidp
|
|
|
19
19
|
|
|
20
20
|
class ConversationError < StandardError; end
|
|
21
21
|
|
|
22
|
-
def initialize(project_dir, prompt: nil, use_enhanced_input: true, verbose: false)
|
|
22
|
+
def initialize(project_dir, prompt: nil, use_enhanced_input: true, verbose: false, config_manager: nil, provider_manager: nil)
|
|
23
23
|
@project_dir = project_dir
|
|
24
24
|
|
|
25
25
|
# Use EnhancedInput with Reline for full readline-style key bindings
|
|
@@ -29,8 +29,8 @@ module Aidp
|
|
|
29
29
|
prompt || TTY::Prompt.new
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
@config_manager = Aidp::Harness::ConfigManager.new(project_dir)
|
|
33
|
-
@provider_manager = Aidp::Harness::ProviderManager.new(@config_manager, prompt: @prompt)
|
|
32
|
+
@config_manager = config_manager || Aidp::Harness::ConfigManager.new(project_dir)
|
|
33
|
+
@provider_manager = provider_manager || Aidp::Harness::ProviderManager.new(@config_manager, prompt: @prompt)
|
|
34
34
|
@conversation_history = []
|
|
35
35
|
@user_input = {}
|
|
36
36
|
@verbose = verbose
|
|
@@ -156,8 +156,11 @@ module Aidp
|
|
|
156
156
|
)
|
|
157
157
|
|
|
158
158
|
# Log error and exit
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
# Suppress backtrace noise during tests while keeping it for production debugging
|
|
160
|
+
unless ENV["RSPEC_RUNNING"] == "true"
|
|
161
|
+
warn("Error in workstream #{slug}: #{e.message}")
|
|
162
|
+
warn(e.backtrace.first(5).join("\n"))
|
|
163
|
+
end
|
|
161
164
|
exit(1)
|
|
162
165
|
end
|
|
163
166
|
|
data/lib/aidp.rb
CHANGED
|
@@ -93,6 +93,10 @@ require_relative "aidp/harness/state_manager"
|
|
|
93
93
|
require_relative "aidp/harness/error_handler"
|
|
94
94
|
require_relative "aidp/harness/status_display"
|
|
95
95
|
require_relative "aidp/harness/runner"
|
|
96
|
+
require_relative "aidp/harness/filter_strategy"
|
|
97
|
+
require_relative "aidp/harness/generic_filter_strategy"
|
|
98
|
+
require_relative "aidp/harness/rspec_filter_strategy"
|
|
99
|
+
require_relative "aidp/harness/output_filter"
|
|
96
100
|
|
|
97
101
|
# UI components
|
|
98
102
|
require_relative "aidp/harness/ui/spinner_helper"
|
|
@@ -137,7 +137,7 @@ providers:
|
|
|
137
137
|
file_upload: true
|
|
138
138
|
code_generation: true
|
|
139
139
|
analysis: true
|
|
140
|
-
|
|
140
|
+
supports_json_mode: true
|
|
141
141
|
|
|
142
142
|
# Monitoring configuration (enhanced for development)
|
|
143
143
|
monitoring:
|
|
@@ -243,7 +243,7 @@ providers:
|
|
|
243
243
|
code_generation: true
|
|
244
244
|
analysis: true
|
|
245
245
|
vision: true
|
|
246
|
-
|
|
246
|
+
supports_json_mode: true
|
|
247
247
|
function_calling: true
|
|
248
248
|
tool_use: true
|
|
249
249
|
|