aidp 0.27.0 → 0.28.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 +89 -0
- data/lib/aidp/cli/models_command.rb +5 -6
- data/lib/aidp/cli.rb +10 -8
- data/lib/aidp/config.rb +54 -0
- data/lib/aidp/debug_mixin.rb +23 -1
- data/lib/aidp/execute/agent_signal_parser.rb +22 -0
- data/lib/aidp/execute/repl_macros.rb +2 -2
- data/lib/aidp/execute/steps.rb +94 -1
- data/lib/aidp/execute/work_loop_runner.rb +209 -17
- data/lib/aidp/execute/workflow_selector.rb +2 -25
- data/lib/aidp/firewall/provider_requirements_collector.rb +262 -0
- data/lib/aidp/harness/ai_decision_engine.rb +35 -2
- data/lib/aidp/harness/config_manager.rb +0 -5
- data/lib/aidp/harness/config_schema.rb +8 -0
- data/lib/aidp/harness/configuration.rb +27 -19
- data/lib/aidp/harness/enhanced_runner.rb +1 -4
- data/lib/aidp/harness/error_handler.rb +1 -72
- data/lib/aidp/harness/provider_factory.rb +11 -2
- data/lib/aidp/harness/state_manager.rb +0 -7
- data/lib/aidp/harness/thinking_depth_manager.rb +47 -68
- data/lib/aidp/harness/ui/enhanced_tui.rb +8 -18
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +0 -18
- data/lib/aidp/harness/ui/progress_display.rb +6 -2
- data/lib/aidp/harness/user_interface.rb +0 -58
- data/lib/aidp/init/runner.rb +7 -2
- data/lib/aidp/planning/analyzers/feedback_analyzer.rb +365 -0
- data/lib/aidp/planning/builders/agile_plan_builder.rb +387 -0
- data/lib/aidp/planning/builders/project_plan_builder.rb +193 -0
- data/lib/aidp/planning/generators/gantt_generator.rb +190 -0
- data/lib/aidp/planning/generators/iteration_plan_generator.rb +392 -0
- data/lib/aidp/planning/generators/legacy_research_planner.rb +473 -0
- data/lib/aidp/planning/generators/marketing_report_generator.rb +348 -0
- data/lib/aidp/planning/generators/mvp_scope_generator.rb +310 -0
- data/lib/aidp/planning/generators/user_test_plan_generator.rb +373 -0
- data/lib/aidp/planning/generators/wbs_generator.rb +259 -0
- data/lib/aidp/planning/mappers/persona_mapper.rb +163 -0
- data/lib/aidp/planning/parsers/document_parser.rb +141 -0
- data/lib/aidp/planning/parsers/feedback_data_parser.rb +252 -0
- data/lib/aidp/provider_manager.rb +8 -32
- data/lib/aidp/providers/aider.rb +264 -0
- data/lib/aidp/providers/anthropic.rb +74 -2
- data/lib/aidp/providers/base.rb +25 -1
- data/lib/aidp/providers/codex.rb +26 -3
- data/lib/aidp/providers/cursor.rb +16 -0
- data/lib/aidp/providers/gemini.rb +13 -0
- data/lib/aidp/providers/github_copilot.rb +17 -0
- data/lib/aidp/providers/kilocode.rb +11 -0
- data/lib/aidp/providers/opencode.rb +11 -0
- data/lib/aidp/setup/wizard.rb +249 -39
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +211 -30
- data/lib/aidp/watch/change_request_processor.rb +128 -14
- data/lib/aidp/watch/ci_fix_processor.rb +103 -37
- data/lib/aidp/watch/ci_log_extractor.rb +258 -0
- data/lib/aidp/watch/github_state_extractor.rb +177 -0
- data/lib/aidp/watch/implementation_verifier.rb +284 -0
- data/lib/aidp/watch/plan_generator.rb +7 -43
- data/lib/aidp/watch/plan_processor.rb +7 -6
- data/lib/aidp/watch/repository_client.rb +245 -17
- data/lib/aidp/watch/review_processor.rb +98 -17
- data/lib/aidp/watch/reviewers/base_reviewer.rb +1 -1
- data/lib/aidp/watch/runner.rb +181 -29
- data/lib/aidp/watch/state_store.rb +22 -1
- data/lib/aidp/workflows/definitions.rb +147 -0
- data/lib/aidp/workstream_cleanup.rb +245 -0
- data/lib/aidp/worktree.rb +19 -0
- data/templates/aidp.yml.example +57 -0
- data/templates/implementation/generate_tdd_specs.md +213 -0
- data/templates/implementation/iterative_implementation.md +122 -0
- data/templates/planning/agile/analyze_feedback.md +183 -0
- data/templates/planning/agile/generate_iteration_plan.md +179 -0
- data/templates/planning/agile/generate_legacy_research_plan.md +171 -0
- data/templates/planning/agile/generate_marketing_report.md +162 -0
- data/templates/planning/agile/generate_mvp_scope.md +127 -0
- data/templates/planning/agile/generate_user_test_plan.md +143 -0
- data/templates/planning/agile/ingest_feedback.md +174 -0
- data/templates/planning/assemble_project_plan.md +113 -0
- data/templates/planning/assign_personas.md +108 -0
- data/templates/planning/create_tasks.md +52 -6
- data/templates/planning/generate_gantt.md +86 -0
- data/templates/planning/generate_wbs.md +85 -0
- data/templates/planning/initialize_planning_mode.md +70 -0
- data/templates/skills/README.md +2 -2
- data/templates/skills/marketing_strategist/SKILL.md +279 -0
- data/templates/skills/product_manager/SKILL.md +177 -0
- data/templates/skills/ruby_aidp_planning/SKILL.md +497 -0
- data/templates/skills/ruby_rspec_tdd/SKILL.md +514 -0
- data/templates/skills/ux_researcher/SKILL.md +222 -0
- metadata +39 -1
|
@@ -122,6 +122,42 @@ module Aidp
|
|
|
122
122
|
|
|
123
123
|
private
|
|
124
124
|
|
|
125
|
+
# Retry a GitHub CLI operation with exponential backoff
|
|
126
|
+
# Rescues network-related RuntimeErrors from gh CLI
|
|
127
|
+
def with_gh_retry(operation_name, max_retries: 3, initial_delay: 1.0)
|
|
128
|
+
retries = 0
|
|
129
|
+
begin
|
|
130
|
+
yield
|
|
131
|
+
rescue RuntimeError => e
|
|
132
|
+
# Only retry on network-related errors from gh CLI
|
|
133
|
+
if e.message.include?("unexpected EOF") ||
|
|
134
|
+
e.message.include?("connection") ||
|
|
135
|
+
e.message.include?("timeout") ||
|
|
136
|
+
e.message.include?("Request to https://api.github.com")
|
|
137
|
+
retries += 1
|
|
138
|
+
if retries <= max_retries
|
|
139
|
+
delay = initial_delay * (2**(retries - 1))
|
|
140
|
+
Aidp.log_warn("repository_client", "gh_retry",
|
|
141
|
+
operation: operation_name,
|
|
142
|
+
attempt: retries,
|
|
143
|
+
max_retries: max_retries,
|
|
144
|
+
delay: delay,
|
|
145
|
+
error: e.message.lines.first&.strip)
|
|
146
|
+
sleep(delay)
|
|
147
|
+
retry
|
|
148
|
+
else
|
|
149
|
+
Aidp.log_error("repository_client", "gh_retry_exhausted",
|
|
150
|
+
operation: operation_name,
|
|
151
|
+
error: e.message)
|
|
152
|
+
raise
|
|
153
|
+
end
|
|
154
|
+
else
|
|
155
|
+
# Non-network errors should not be retried
|
|
156
|
+
raise
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
125
161
|
def list_issues_via_gh(labels:, state:)
|
|
126
162
|
json_fields = %w[number title labels updatedAt state url assignees]
|
|
127
163
|
cmd = ["gh", "issue", "list", "--repo", full_repo, "--state", state, "--json", json_fields.join(",")]
|
|
@@ -156,14 +192,16 @@ module Aidp
|
|
|
156
192
|
end
|
|
157
193
|
|
|
158
194
|
def fetch_issue_via_gh(number)
|
|
159
|
-
|
|
160
|
-
|
|
195
|
+
with_gh_retry("fetch_issue") do
|
|
196
|
+
fields = %w[number title body comments labels state assignees url updatedAt author]
|
|
197
|
+
cmd = ["gh", "issue", "view", number.to_s, "--repo", full_repo, "--json", fields.join(",")]
|
|
161
198
|
|
|
162
|
-
|
|
163
|
-
|
|
199
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
200
|
+
raise "GitHub CLI error: #{stderr.strip}" unless status.success?
|
|
164
201
|
|
|
165
|
-
|
|
166
|
-
|
|
202
|
+
data = JSON.parse(stdout)
|
|
203
|
+
normalize_issue_detail(data)
|
|
204
|
+
end
|
|
167
205
|
rescue JSON::ParserError => e
|
|
168
206
|
raise "Failed to parse GitHub CLI issue response: #{e.message}"
|
|
169
207
|
end
|
|
@@ -240,6 +278,33 @@ module Aidp
|
|
|
240
278
|
end
|
|
241
279
|
|
|
242
280
|
def create_pull_request_via_gh(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil)
|
|
281
|
+
Aidp.log_debug(
|
|
282
|
+
"repository_client",
|
|
283
|
+
"preparing_gh_pr_create",
|
|
284
|
+
repo: full_repo,
|
|
285
|
+
head: head,
|
|
286
|
+
base: base,
|
|
287
|
+
draft: draft,
|
|
288
|
+
assignee: assignee,
|
|
289
|
+
issue_number: issue_number,
|
|
290
|
+
gh_available: gh_available?,
|
|
291
|
+
title_length: title.length,
|
|
292
|
+
body_length: body.length
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
unless gh_available?
|
|
296
|
+
error_msg = "GitHub CLI (gh) is not available - cannot create PR"
|
|
297
|
+
Aidp.log_error(
|
|
298
|
+
"repository_client",
|
|
299
|
+
"gh_cli_not_available",
|
|
300
|
+
repo: full_repo,
|
|
301
|
+
head: head,
|
|
302
|
+
base: base,
|
|
303
|
+
message: error_msg
|
|
304
|
+
)
|
|
305
|
+
raise error_msg
|
|
306
|
+
end
|
|
307
|
+
|
|
243
308
|
cmd = [
|
|
244
309
|
"gh", "pr", "create",
|
|
245
310
|
"--repo", full_repo,
|
|
@@ -251,8 +316,56 @@ module Aidp
|
|
|
251
316
|
cmd += ["--draft"] if draft
|
|
252
317
|
cmd += ["--assignee", assignee] if assignee
|
|
253
318
|
|
|
319
|
+
Aidp.log_debug(
|
|
320
|
+
"repository_client",
|
|
321
|
+
"executing_gh_pr_create",
|
|
322
|
+
repo: full_repo,
|
|
323
|
+
head: head,
|
|
324
|
+
base: base,
|
|
325
|
+
draft: draft,
|
|
326
|
+
assignee: assignee,
|
|
327
|
+
command: cmd.join(" ")
|
|
328
|
+
)
|
|
329
|
+
|
|
254
330
|
stdout, stderr, status = Open3.capture3(*cmd)
|
|
255
|
-
|
|
331
|
+
|
|
332
|
+
Aidp.log_debug(
|
|
333
|
+
"repository_client",
|
|
334
|
+
"gh_pr_create_result",
|
|
335
|
+
repo: full_repo,
|
|
336
|
+
success: status.success?,
|
|
337
|
+
exit_code: status.exitstatus,
|
|
338
|
+
stdout_length: stdout.length,
|
|
339
|
+
stderr_length: stderr.length,
|
|
340
|
+
stdout_preview: stdout[0, 200],
|
|
341
|
+
stderr: stderr
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
unless status.success?
|
|
345
|
+
Aidp.log_error(
|
|
346
|
+
"repository_client",
|
|
347
|
+
"gh_pr_create_failed",
|
|
348
|
+
repo: full_repo,
|
|
349
|
+
head: head,
|
|
350
|
+
base: base,
|
|
351
|
+
issue_number: issue_number,
|
|
352
|
+
exit_code: status.exitstatus,
|
|
353
|
+
stderr: stderr,
|
|
354
|
+
stdout: stdout,
|
|
355
|
+
command: cmd.join(" ")
|
|
356
|
+
)
|
|
357
|
+
raise "Failed to create PR via gh: #{stderr.strip}"
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
Aidp.log_info(
|
|
361
|
+
"repository_client",
|
|
362
|
+
"gh_pr_create_success",
|
|
363
|
+
repo: full_repo,
|
|
364
|
+
head: head,
|
|
365
|
+
base: base,
|
|
366
|
+
issue_number: issue_number,
|
|
367
|
+
output_preview: stdout[0, 200]
|
|
368
|
+
)
|
|
256
369
|
|
|
257
370
|
stdout.strip
|
|
258
371
|
end
|
|
@@ -476,14 +589,19 @@ module Aidp
|
|
|
476
589
|
cmd = ["gh", "api", "repos/#{full_repo}/commits/#{head_sha}/check-runs", "--jq", "."]
|
|
477
590
|
stdout, _stderr, status = Open3.capture3(*cmd)
|
|
478
591
|
|
|
479
|
-
if status.success?
|
|
592
|
+
check_runs = if status.success?
|
|
480
593
|
data = JSON.parse(stdout)
|
|
481
|
-
|
|
482
|
-
normalize_ci_status(check_runs, head_sha)
|
|
594
|
+
data["check_runs"] || []
|
|
483
595
|
else
|
|
484
|
-
|
|
485
|
-
|
|
596
|
+
Aidp.log_warn("repository_client", "Failed to fetch check runs", sha: head_sha)
|
|
597
|
+
[]
|
|
486
598
|
end
|
|
599
|
+
|
|
600
|
+
# Also fetch commit statuses (for OAuth apps, webhooks, etc.)
|
|
601
|
+
commit_statuses = fetch_commit_statuses_via_gh(head_sha)
|
|
602
|
+
|
|
603
|
+
# Combine and normalize both check runs and commit statuses
|
|
604
|
+
normalize_ci_status_combined(check_runs, commit_statuses, head_sha)
|
|
487
605
|
rescue => e
|
|
488
606
|
Aidp.log_warn("repository_client", "Failed to fetch CI status", error: e.message)
|
|
489
607
|
{sha: nil, state: "unknown", checks: []}
|
|
@@ -501,13 +619,19 @@ module Aidp
|
|
|
501
619
|
http.request(request)
|
|
502
620
|
end
|
|
503
621
|
|
|
504
|
-
if response.code == "200"
|
|
622
|
+
check_runs = if response.code == "200"
|
|
505
623
|
data = JSON.parse(response.body)
|
|
506
|
-
|
|
507
|
-
normalize_ci_status(check_runs, head_sha)
|
|
624
|
+
data["check_runs"] || []
|
|
508
625
|
else
|
|
509
|
-
|
|
626
|
+
Aidp.log_warn("repository_client", "Failed to fetch check runs via API", sha: head_sha, code: response.code)
|
|
627
|
+
[]
|
|
510
628
|
end
|
|
629
|
+
|
|
630
|
+
# Also fetch commit statuses (for OAuth apps, webhooks, etc.)
|
|
631
|
+
commit_statuses = fetch_commit_statuses_via_api(head_sha)
|
|
632
|
+
|
|
633
|
+
# Combine and normalize both check runs and commit statuses
|
|
634
|
+
normalize_ci_status_combined(check_runs, commit_statuses, head_sha)
|
|
511
635
|
rescue => e
|
|
512
636
|
Aidp.log_warn("repository_client", "Failed to fetch CI status", error: e.message)
|
|
513
637
|
{sha: nil, state: "unknown", checks: []}
|
|
@@ -660,6 +784,85 @@ module Aidp
|
|
|
660
784
|
}
|
|
661
785
|
end
|
|
662
786
|
|
|
787
|
+
def fetch_commit_statuses_via_gh(head_sha)
|
|
788
|
+
cmd = ["gh", "api", "repos/#{full_repo}/commits/#{head_sha}/status", "--jq", "."]
|
|
789
|
+
stdout, _stderr, status = Open3.capture3(*cmd)
|
|
790
|
+
|
|
791
|
+
if status.success?
|
|
792
|
+
data = JSON.parse(stdout)
|
|
793
|
+
data["statuses"] || []
|
|
794
|
+
else
|
|
795
|
+
Aidp.log_debug("repository_client", "No commit statuses found", sha: head_sha)
|
|
796
|
+
[]
|
|
797
|
+
end
|
|
798
|
+
rescue => e
|
|
799
|
+
Aidp.log_warn("repository_client", "Failed to fetch commit statuses", error: e.message, sha: head_sha)
|
|
800
|
+
[]
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def fetch_commit_statuses_via_api(head_sha)
|
|
804
|
+
uri = URI("https://api.github.com/repos/#{full_repo}/commits/#{head_sha}/status")
|
|
805
|
+
request = Net::HTTP::Get.new(uri)
|
|
806
|
+
request["Accept"] = "application/vnd.github.v3+json"
|
|
807
|
+
|
|
808
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
809
|
+
http.request(request)
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
if response.code == "200"
|
|
813
|
+
data = JSON.parse(response.body)
|
|
814
|
+
data["statuses"] || []
|
|
815
|
+
else
|
|
816
|
+
Aidp.log_debug("repository_client", "No commit statuses found via API", sha: head_sha, code: response.code)
|
|
817
|
+
[]
|
|
818
|
+
end
|
|
819
|
+
rescue => e
|
|
820
|
+
Aidp.log_warn("repository_client", "Failed to fetch commit statuses via API", error: e.message, sha: head_sha)
|
|
821
|
+
[]
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
def normalize_ci_status_combined(check_runs, commit_statuses, head_sha)
|
|
825
|
+
# Convert commit statuses to same format as check runs for unified processing
|
|
826
|
+
# normalize_ci_status expects string keys, so we use string keys here
|
|
827
|
+
checks_from_statuses = commit_statuses.map do |status|
|
|
828
|
+
{
|
|
829
|
+
"name" => status["context"],
|
|
830
|
+
"status" => (status["state"] == "pending") ? "in_progress" : "completed",
|
|
831
|
+
"conclusion" => normalize_commit_status_to_conclusion(status["state"]),
|
|
832
|
+
"details_url" => status["target_url"],
|
|
833
|
+
"output" => status["description"] ? {"summary" => status["description"]} : nil
|
|
834
|
+
}
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
# Combine check runs and converted commit statuses
|
|
838
|
+
# check_runs should already have string keys from API/CLI responses
|
|
839
|
+
all_checks = check_runs + checks_from_statuses
|
|
840
|
+
|
|
841
|
+
Aidp.log_debug("repository_client", "combined_ci_checks",
|
|
842
|
+
check_run_count: check_runs.length,
|
|
843
|
+
commit_status_count: commit_statuses.length,
|
|
844
|
+
total_checks: all_checks.length)
|
|
845
|
+
|
|
846
|
+
# Use existing normalize logic
|
|
847
|
+
normalize_ci_status(all_checks, head_sha)
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def normalize_commit_status_to_conclusion(state)
|
|
851
|
+
# Map commit status states to check run conclusions
|
|
852
|
+
# Commit status states: error, failure, pending, success
|
|
853
|
+
# Check run conclusions: success, failure, neutral, cancelled, timed_out, action_required, skipped, stale
|
|
854
|
+
case state
|
|
855
|
+
when "success"
|
|
856
|
+
"success"
|
|
857
|
+
when "failure"
|
|
858
|
+
"failure"
|
|
859
|
+
when "error"
|
|
860
|
+
"failure" # Treat error as failure for consistency
|
|
861
|
+
when "pending", nil
|
|
862
|
+
nil # pending means not completed yet
|
|
863
|
+
end
|
|
864
|
+
end
|
|
865
|
+
|
|
663
866
|
def normalize_ci_status(check_runs, head_sha)
|
|
664
867
|
checks = check_runs.map do |run|
|
|
665
868
|
{
|
|
@@ -671,17 +874,42 @@ module Aidp
|
|
|
671
874
|
}
|
|
672
875
|
end
|
|
673
876
|
|
|
877
|
+
Aidp.log_debug("repository_client", "normalize_ci_status",
|
|
878
|
+
check_count: checks.length,
|
|
879
|
+
checks: checks.map { |c| {name: c[:name], status: c[:status], conclusion: c[:conclusion]} })
|
|
880
|
+
|
|
674
881
|
# Determine overall state
|
|
675
|
-
|
|
882
|
+
# CRITICAL: Empty check list should be "unknown", not "success"
|
|
883
|
+
# This guard must come FIRST to prevent vacuous truth from [].all? { condition }
|
|
884
|
+
state = if checks.empty?
|
|
885
|
+
Aidp.log_debug("repository_client", "ci_status_empty", message: "No checks found")
|
|
886
|
+
"unknown"
|
|
887
|
+
elsif checks.any? { |c| c[:conclusion] == "failure" }
|
|
888
|
+
failing_checks = checks.select { |c| c[:conclusion] == "failure" }
|
|
889
|
+
Aidp.log_debug("repository_client", "ci_status_failure",
|
|
890
|
+
failing_count: failing_checks.length,
|
|
891
|
+
failing_checks: failing_checks.map { |c| c[:name] })
|
|
676
892
|
"failure"
|
|
677
893
|
elsif checks.any? { |c| c[:status] != "completed" }
|
|
894
|
+
pending_checks = checks.select { |c| c[:status] != "completed" }
|
|
895
|
+
Aidp.log_debug("repository_client", "ci_status_pending",
|
|
896
|
+
pending_count: pending_checks.length,
|
|
897
|
+
pending_checks: pending_checks.map { |c| {name: c[:name], status: c[:status]} })
|
|
678
898
|
"pending"
|
|
679
899
|
elsif checks.all? { |c| c[:conclusion] == "success" }
|
|
900
|
+
Aidp.log_debug("repository_client", "ci_status_success",
|
|
901
|
+
success_count: checks.length)
|
|
680
902
|
"success"
|
|
681
903
|
else
|
|
904
|
+
non_success_checks = checks.reject { |c| c[:conclusion] == "success" }
|
|
905
|
+
Aidp.log_debug("repository_client", "ci_status_unknown",
|
|
906
|
+
non_success_count: non_success_checks.length,
|
|
907
|
+
non_success_checks: non_success_checks.map { |c| {name: c[:name], conclusion: c[:conclusion]} })
|
|
682
908
|
"unknown"
|
|
683
909
|
end
|
|
684
910
|
|
|
911
|
+
Aidp.log_debug("repository_client", "ci_status_determined", sha: head_sha, state: state)
|
|
912
|
+
|
|
685
913
|
{
|
|
686
914
|
sha: head_sha,
|
|
687
915
|
state: state,
|
|
@@ -5,6 +5,8 @@ require "json"
|
|
|
5
5
|
require "time"
|
|
6
6
|
|
|
7
7
|
require_relative "../message_display"
|
|
8
|
+
require_relative "github_state_extractor"
|
|
9
|
+
require_relative "implementation_verifier"
|
|
8
10
|
require_relative "reviewers/senior_dev_reviewer"
|
|
9
11
|
require_relative "reviewers/security_reviewer"
|
|
10
12
|
require_relative "reviewers/performance_reviewer"
|
|
@@ -23,9 +25,10 @@ module Aidp
|
|
|
23
25
|
|
|
24
26
|
attr_reader :review_label
|
|
25
27
|
|
|
26
|
-
def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, verbose: false, reviewers: nil)
|
|
28
|
+
def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, verbose: false, reviewers: nil, verifier: nil)
|
|
27
29
|
@repository_client = repository_client
|
|
28
30
|
@state_store = state_store
|
|
31
|
+
@state_extractor = GitHubStateExtractor.new(repository_client: repository_client)
|
|
29
32
|
@provider_name = provider_name
|
|
30
33
|
@project_dir = project_dir
|
|
31
34
|
@verbose = verbose
|
|
@@ -33,6 +36,12 @@ module Aidp
|
|
|
33
36
|
# Load label configuration
|
|
34
37
|
@review_label = label_config[:review_trigger] || label_config["review_trigger"] || DEFAULT_REVIEW_LABEL
|
|
35
38
|
|
|
39
|
+
# Initialize verifier (allow dependency injection for testing)
|
|
40
|
+
@verifier = verifier || ImplementationVerifier.new(
|
|
41
|
+
repository_client: repository_client,
|
|
42
|
+
project_dir: project_dir
|
|
43
|
+
)
|
|
44
|
+
|
|
36
45
|
# Initialize reviewers (allow dependency injection for testing)
|
|
37
46
|
@reviewers = reviewers || [
|
|
38
47
|
Reviewers::SeniorDevReviewer.new(provider_name: provider_name),
|
|
@@ -44,7 +53,8 @@ module Aidp
|
|
|
44
53
|
def process(pr)
|
|
45
54
|
number = pr[:number]
|
|
46
55
|
|
|
47
|
-
if
|
|
56
|
+
# Check if review already completed via GitHub comments
|
|
57
|
+
if @state_extractor.review_completed?(pr)
|
|
48
58
|
display_message("ℹ️ Review for PR ##{number} already posted. Skipping.", type: :muted)
|
|
49
59
|
return
|
|
50
60
|
end
|
|
@@ -56,6 +66,9 @@ module Aidp
|
|
|
56
66
|
files = @repository_client.fetch_pull_request_files(number)
|
|
57
67
|
diff = @repository_client.fetch_pull_request_diff(number)
|
|
58
68
|
|
|
69
|
+
# Check if PR is linked to an issue - if so, run implementation verification
|
|
70
|
+
verification_result = check_implementation_completeness(pr_data)
|
|
71
|
+
|
|
59
72
|
# Run reviews in parallel (conceptually - actual implementation is sequential)
|
|
60
73
|
review_results = run_reviews(pr_data: pr_data, files: files, diff: diff)
|
|
61
74
|
|
|
@@ -63,7 +76,11 @@ module Aidp
|
|
|
63
76
|
log_review(number, review_results)
|
|
64
77
|
|
|
65
78
|
# Format and post comment
|
|
66
|
-
comment_body = format_review_comment(
|
|
79
|
+
comment_body = format_review_comment(
|
|
80
|
+
pr: pr_data,
|
|
81
|
+
review_results: review_results,
|
|
82
|
+
verification_result: verification_result
|
|
83
|
+
)
|
|
67
84
|
@repository_client.post_comment(number, comment_body)
|
|
68
85
|
|
|
69
86
|
display_message("💬 Posted review comment for PR ##{number}", type: :success)
|
|
@@ -84,19 +101,14 @@ module Aidp
|
|
|
84
101
|
display_message("❌ Review failed: #{e.message}", type: :error)
|
|
85
102
|
Aidp.log_error("review_processor", "Review failed", pr: number, error: e.message, backtrace: e.backtrace&.first(10))
|
|
86
103
|
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
begin
|
|
96
|
-
@repository_client.post_comment(number, error_comment)
|
|
97
|
-
rescue
|
|
98
|
-
nil
|
|
99
|
-
end
|
|
104
|
+
# Record failure state internally but DON'T post error to GitHub
|
|
105
|
+
# (per issue #280 - error messages should never appear on issues)
|
|
106
|
+
@state_store.record_review(number, {
|
|
107
|
+
status: "error",
|
|
108
|
+
error: e.message,
|
|
109
|
+
error_class: e.class.name,
|
|
110
|
+
timestamp: Time.now.utc.iso8601
|
|
111
|
+
})
|
|
100
112
|
end
|
|
101
113
|
|
|
102
114
|
private
|
|
@@ -123,13 +135,82 @@ module Aidp
|
|
|
123
135
|
results
|
|
124
136
|
end
|
|
125
137
|
|
|
126
|
-
def
|
|
138
|
+
def check_implementation_completeness(pr_data)
|
|
139
|
+
# Extract linked issue from PR description
|
|
140
|
+
issue_number = @state_extractor.extract_linked_issue(pr_data[:body])
|
|
141
|
+
|
|
142
|
+
unless issue_number
|
|
143
|
+
display_message(" ℹ️ No linked issue found - skipping implementation verification", type: :muted) if @verbose
|
|
144
|
+
return nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
display_message(" 🔗 Found linked issue ##{issue_number} - verifying implementation...", type: :info) if @verbose
|
|
148
|
+
|
|
149
|
+
begin
|
|
150
|
+
# Fetch the linked issue
|
|
151
|
+
issue = @repository_client.fetch_issue(issue_number)
|
|
152
|
+
|
|
153
|
+
# Check if a worktree exists for this PR branch
|
|
154
|
+
working_dir = find_or_use_worktree(pr_data[:head_ref])
|
|
155
|
+
|
|
156
|
+
# Run verification
|
|
157
|
+
result = @verifier.verify(issue: issue, working_dir: working_dir)
|
|
158
|
+
|
|
159
|
+
if result[:verified]
|
|
160
|
+
display_message(" ✅ Implementation verified complete", type: :success) if @verbose
|
|
161
|
+
else
|
|
162
|
+
display_message(" ⚠️ Implementation appears incomplete", type: :warn)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
result
|
|
166
|
+
rescue => e
|
|
167
|
+
display_message(" ⚠️ Verification check failed: #{e.message}", type: :warn)
|
|
168
|
+
Aidp.log_error("review_processor", "Verification failed", issue: issue_number, error: e.message)
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def find_or_use_worktree(branch)
|
|
174
|
+
# Check if a worktree already exists for this branch
|
|
175
|
+
existing = Aidp::Worktree.find_by_branch(branch: branch, project_dir: @project_dir)
|
|
176
|
+
|
|
177
|
+
if existing && existing[:active]
|
|
178
|
+
display_message(" 🔄 Using existing worktree for branch: #{branch}", type: :muted) if @verbose
|
|
179
|
+
return existing[:path]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Otherwise, use the main project directory
|
|
183
|
+
# (assuming the branch is checked out in the main directory)
|
|
184
|
+
@project_dir
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def format_review_comment(pr:, review_results:, verification_result: nil)
|
|
127
188
|
parts = []
|
|
128
189
|
parts << COMMENT_HEADER
|
|
129
190
|
parts << ""
|
|
130
191
|
parts << "Automated multi-persona code review for PR ##{pr[:number]}"
|
|
131
192
|
parts << ""
|
|
132
193
|
|
|
194
|
+
# Add verification results if present
|
|
195
|
+
if verification_result
|
|
196
|
+
if verification_result[:verified]
|
|
197
|
+
parts << "### ✅ Implementation Verification"
|
|
198
|
+
parts << ""
|
|
199
|
+
parts << "_Implementation successfully verified against linked issue requirements._"
|
|
200
|
+
else
|
|
201
|
+
parts << "### ⚠️ Implementation Incomplete"
|
|
202
|
+
parts << ""
|
|
203
|
+
parts << "**This PR appears to be incomplete based on the linked issue requirements:**"
|
|
204
|
+
parts << ""
|
|
205
|
+
verification_result[:reasons]&.each do |reason|
|
|
206
|
+
parts << "- #{reason}"
|
|
207
|
+
end
|
|
208
|
+
parts << ""
|
|
209
|
+
parts << "**Suggested Action:** Add the `aidp-request-changes` label if you'd like AIDP to help complete the implementation."
|
|
210
|
+
end
|
|
211
|
+
parts << ""
|
|
212
|
+
end
|
|
213
|
+
|
|
133
214
|
# Collect all findings by severity
|
|
134
215
|
all_findings = collect_findings_by_severity(review_results)
|
|
135
216
|
|