aidp 0.26.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/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 +661 -0
- data/lib/aidp/cli/providers_command.rb +223 -0
- data/lib/aidp/cli.rb +45 -464
- data/lib/aidp/config.rb +54 -0
- data/lib/aidp/daemon/runner.rb +2 -2
- data/lib/aidp/debug_mixin.rb +25 -10
- data/lib/aidp/execute/agent_signal_parser.rb +22 -0
- 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/repl_macros.rb +2 -2
- data/lib/aidp/execute/steps.rb +94 -1
- data/lib/aidp/execute/work_loop_runner.rb +238 -19
- data/lib/aidp/execute/workflow_selector.rb +4 -27
- 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 +5 -10
- data/lib/aidp/harness/config_schema.rb +8 -0
- data/lib/aidp/harness/configuration.rb +40 -2
- data/lib/aidp/harness/enhanced_runner.rb +25 -19
- data/lib/aidp/harness/error_handler.rb +23 -73
- 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/provider_factory.rb +11 -2
- data/lib/aidp/harness/runner.rb +5 -0
- data/lib/aidp/harness/state_manager.rb +0 -7
- data/lib/aidp/harness/thinking_depth_manager.rb +202 -7
- 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/message_display.rb +0 -46
- 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/adapter.rb +2 -4
- data/lib/aidp/providers/aider.rb +264 -0
- data/lib/aidp/providers/anthropic.rb +206 -121
- data/lib/aidp/providers/base.rb +123 -3
- data/lib/aidp/providers/capability_registry.rb +0 -1
- data/lib/aidp/providers/codex.rb +75 -70
- data/lib/aidp/providers/cursor.rb +87 -59
- data/lib/aidp/providers/gemini.rb +57 -60
- data/lib/aidp/providers/github_copilot.rb +19 -66
- data/lib/aidp/providers/kilocode.rb +35 -80
- data/lib/aidp/providers/opencode.rb +35 -80
- data/lib/aidp/setup/wizard.rb +555 -8
- 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 +95 -52
- 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 +100 -19
- 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/workflows/guided_agent.rb +3 -3
- data/lib/aidp/workstream_cleanup.rb +245 -0
- data/lib/aidp/worktree.rb +19 -0
- data/templates/aidp-development.yml.example +2 -2
- data/templates/aidp-production.yml.example +3 -3
- 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 +47 -1
|
@@ -11,6 +11,9 @@ require_relative "../harness/config_manager"
|
|
|
11
11
|
require_relative "../execute/prompt_manager"
|
|
12
12
|
require_relative "../harness/runner"
|
|
13
13
|
require_relative "../harness/state_manager"
|
|
14
|
+
require_relative "../worktree"
|
|
15
|
+
require_relative "github_state_extractor"
|
|
16
|
+
require_relative "ci_log_extractor"
|
|
14
17
|
|
|
15
18
|
module Aidp
|
|
16
19
|
module Watch
|
|
@@ -30,6 +33,7 @@ module Aidp
|
|
|
30
33
|
def initialize(repository_client:, state_store:, provider_name: nil, project_dir: Dir.pwd, label_config: {}, verbose: false)
|
|
31
34
|
@repository_client = repository_client
|
|
32
35
|
@state_store = state_store
|
|
36
|
+
@state_extractor = GitHubStateExtractor.new(repository_client: repository_client)
|
|
33
37
|
@provider_name = provider_name
|
|
34
38
|
@project_dir = project_dir
|
|
35
39
|
@verbose = verbose
|
|
@@ -41,9 +45,12 @@ module Aidp
|
|
|
41
45
|
def process(pr)
|
|
42
46
|
number = pr[:number]
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
Aidp.log_debug("ci_fix_processor", "process_started", pr_number: number, pr_title: pr[:title])
|
|
49
|
+
|
|
50
|
+
# Check if already processed successfully via GitHub comments
|
|
51
|
+
if @state_extractor.ci_fix_completed?(pr)
|
|
46
52
|
display_message("ℹ️ CI fix for PR ##{number} already completed. Skipping.", type: :muted)
|
|
53
|
+
Aidp.log_debug("ci_fix_processor", "already_completed", pr_number: number)
|
|
47
54
|
return
|
|
48
55
|
end
|
|
49
56
|
|
|
@@ -53,9 +60,16 @@ module Aidp
|
|
|
53
60
|
pr_data = @repository_client.fetch_pull_request(number)
|
|
54
61
|
ci_status = @repository_client.fetch_ci_status(number)
|
|
55
62
|
|
|
63
|
+
Aidp.log_debug("ci_fix_processor", "ci_status_fetched",
|
|
64
|
+
pr_number: number,
|
|
65
|
+
ci_state: ci_status[:state],
|
|
66
|
+
check_count: ci_status[:checks]&.length || 0,
|
|
67
|
+
checks: ci_status[:checks]&.map { |c| {name: c[:name], status: c[:status], conclusion: c[:conclusion]} })
|
|
68
|
+
|
|
56
69
|
# Check if there are failures
|
|
57
70
|
if ci_status[:state] == "success"
|
|
58
71
|
display_message("✅ CI is passing for PR ##{number}. No fixes needed.", type: :success)
|
|
72
|
+
Aidp.log_debug("ci_fix_processor", "ci_passing", pr_number: number)
|
|
59
73
|
post_success_comment(pr_data)
|
|
60
74
|
@state_store.record_ci_fix(number, {status: "no_failures", timestamp: Time.now.utc.iso8601})
|
|
61
75
|
begin
|
|
@@ -68,14 +82,25 @@ module Aidp
|
|
|
68
82
|
|
|
69
83
|
if ci_status[:state] == "pending"
|
|
70
84
|
display_message("⏳ CI is still running for PR ##{number}. Skipping for now.", type: :muted)
|
|
85
|
+
Aidp.log_debug("ci_fix_processor", "ci_pending", pr_number: number)
|
|
71
86
|
return
|
|
72
87
|
end
|
|
73
88
|
|
|
74
89
|
# Get failed checks
|
|
75
90
|
failed_checks = ci_status[:checks].select { |check| check[:conclusion] == "failure" }
|
|
76
91
|
|
|
92
|
+
Aidp.log_debug("ci_fix_processor", "failed_checks_filtered",
|
|
93
|
+
pr_number: number,
|
|
94
|
+
total_checks: ci_status[:checks]&.length || 0,
|
|
95
|
+
failed_count: failed_checks.length,
|
|
96
|
+
failed_checks: failed_checks.map { |c| c[:name] })
|
|
97
|
+
|
|
77
98
|
if failed_checks.empty?
|
|
78
99
|
display_message("⚠️ No specific failed checks found for PR ##{number}.", type: :warn)
|
|
100
|
+
Aidp.log_debug("ci_fix_processor", "no_failed_checks",
|
|
101
|
+
pr_number: number,
|
|
102
|
+
ci_state: ci_status[:state],
|
|
103
|
+
all_checks: ci_status[:checks]&.map { |c| {name: c[:name], conclusion: c[:conclusion]} })
|
|
79
104
|
return
|
|
80
105
|
end
|
|
81
106
|
|
|
@@ -99,31 +124,36 @@ module Aidp
|
|
|
99
124
|
display_message("❌ CI fix failed: #{e.message}", type: :error)
|
|
100
125
|
Aidp.log_error("ci_fix_processor", "CI fix failed", pr: pr[:number], error: e.message, backtrace: e.backtrace&.first(10))
|
|
101
126
|
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
begin
|
|
111
|
-
@repository_client.post_comment(pr[:number], error_comment)
|
|
112
|
-
rescue
|
|
113
|
-
nil
|
|
114
|
-
end
|
|
127
|
+
# Record failure state internally but DON'T post error to GitHub
|
|
128
|
+
# (per issue #280 - error messages should never appear on issues)
|
|
129
|
+
@state_store.record_ci_fix(pr[:number], {
|
|
130
|
+
status: "error",
|
|
131
|
+
error: e.message,
|
|
132
|
+
error_class: e.class.name,
|
|
133
|
+
timestamp: Time.now.utc.iso8601
|
|
134
|
+
})
|
|
115
135
|
end
|
|
116
136
|
|
|
117
137
|
private
|
|
118
138
|
|
|
119
139
|
def analyze_and_fix(pr_data:, ci_status:, failed_checks:)
|
|
120
|
-
#
|
|
140
|
+
# Extract concise failure information to reduce token usage
|
|
141
|
+
provider = detect_default_provider
|
|
142
|
+
provider_manager = Aidp::ProviderManager.get_provider(provider)
|
|
143
|
+
log_extractor = CiLogExtractor.new(provider_manager: provider_manager)
|
|
144
|
+
|
|
121
145
|
failure_details = failed_checks.map do |check|
|
|
146
|
+
Aidp.log_debug("ci_fix_processor", "extracting_logs", check_name: check[:name])
|
|
147
|
+
extracted = log_extractor.extract_failure_info(
|
|
148
|
+
check: check,
|
|
149
|
+
check_run_url: check[:details_url]
|
|
150
|
+
)
|
|
151
|
+
|
|
122
152
|
{
|
|
123
153
|
name: check[:name],
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
154
|
+
summary: extracted[:summary],
|
|
155
|
+
details: extracted[:details],
|
|
156
|
+
extraction_method: extracted[:extraction_method]
|
|
127
157
|
}
|
|
128
158
|
end
|
|
129
159
|
|
|
@@ -131,14 +161,14 @@ module Aidp
|
|
|
131
161
|
analysis = analyze_failures_with_ai(pr_data: pr_data, failures: failure_details)
|
|
132
162
|
|
|
133
163
|
if analysis[:can_fix]
|
|
134
|
-
#
|
|
135
|
-
|
|
164
|
+
# Setup worktree for the PR branch
|
|
165
|
+
working_dir = setup_pr_worktree(pr_data)
|
|
136
166
|
|
|
137
167
|
# Apply the proposed fixes
|
|
138
|
-
apply_fixes(analysis[:fixes])
|
|
168
|
+
apply_fixes(analysis[:fixes], working_dir: working_dir)
|
|
139
169
|
|
|
140
170
|
# Commit and push
|
|
141
|
-
if commit_and_push(pr_data, analysis)
|
|
171
|
+
if commit_and_push(pr_data, analysis, working_dir: working_dir)
|
|
142
172
|
{success: true, analysis: analysis, commit_created: true}
|
|
143
173
|
else
|
|
144
174
|
{success: false, analysis: analysis, reason: "No changes to commit"}
|
|
@@ -152,7 +182,7 @@ module Aidp
|
|
|
152
182
|
|
|
153
183
|
def analyze_failures_with_ai(pr_data:, failures:)
|
|
154
184
|
provider_name = @provider_name || detect_default_provider
|
|
155
|
-
provider = Aidp::ProviderManager.get_provider(provider_name
|
|
185
|
+
provider = Aidp::ProviderManager.get_provider(provider_name)
|
|
156
186
|
|
|
157
187
|
user_prompt = build_ci_analysis_prompt(pr_data: pr_data, failures: failures)
|
|
158
188
|
full_prompt = "#{ci_fix_system_prompt}\n\n#{user_prompt}"
|
|
@@ -222,12 +252,20 @@ module Aidp
|
|
|
222
252
|
#{pr_data[:body]}
|
|
223
253
|
|
|
224
254
|
**Failed Checks:**
|
|
225
|
-
#{failures.map { |f|
|
|
255
|
+
#{failures.map { |f| format_failure_for_prompt(f) }.join("\n\n")}
|
|
226
256
|
|
|
227
257
|
Please analyze these failures and propose fixes if possible.
|
|
228
258
|
PROMPT
|
|
229
259
|
end
|
|
230
260
|
|
|
261
|
+
def format_failure_for_prompt(failure)
|
|
262
|
+
output = "**Check: #{failure[:name]}**\n"
|
|
263
|
+
output += "Summary: #{failure[:summary]}\n" if failure[:summary]
|
|
264
|
+
output += "\nDetails:\n```\n#{failure[:details]}\n```" if failure[:details]
|
|
265
|
+
output += "\n(Logs extracted using: #{failure[:extraction_method]})" if failure[:extraction_method]
|
|
266
|
+
output
|
|
267
|
+
end
|
|
268
|
+
|
|
231
269
|
def detect_default_provider
|
|
232
270
|
config_manager = Aidp::Harness::ConfigManager.new(@project_dir)
|
|
233
271
|
config_manager.default_provider || "anthropic"
|
|
@@ -261,26 +299,54 @@ module Aidp
|
|
|
261
299
|
end
|
|
262
300
|
end
|
|
263
301
|
|
|
264
|
-
def
|
|
302
|
+
def setup_pr_worktree(pr_data)
|
|
265
303
|
head_ref = pr_data[:head_ref]
|
|
304
|
+
pr_number = pr_data[:number]
|
|
305
|
+
|
|
306
|
+
# Check if a worktree already exists for this branch
|
|
307
|
+
existing = Aidp::Worktree.find_by_branch(branch: head_ref, project_dir: @project_dir)
|
|
308
|
+
|
|
309
|
+
if existing && existing[:active]
|
|
310
|
+
display_message("🔄 Reusing existing worktree for branch: #{head_ref}", type: :info)
|
|
311
|
+
Aidp.log_debug("ci_fix_processor", "worktree_reused", pr_number: pr_number, branch: head_ref, path: existing[:path])
|
|
312
|
+
|
|
313
|
+
# Pull latest changes in the worktree
|
|
314
|
+
Dir.chdir(existing[:path]) do
|
|
315
|
+
run_git(%w[fetch origin], allow_failure: true)
|
|
316
|
+
run_git(["checkout", head_ref])
|
|
317
|
+
run_git(%w[pull --ff-only], allow_failure: true)
|
|
318
|
+
end
|
|
266
319
|
|
|
320
|
+
return existing[:path]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Create a new worktree for this PR
|
|
324
|
+
slug = "pr-#{pr_number}-ci-fix"
|
|
325
|
+
display_message("🌿 Creating worktree for PR ##{pr_number}: #{head_ref}", type: :info)
|
|
326
|
+
|
|
327
|
+
# Fetch the branch first
|
|
267
328
|
Dir.chdir(@project_dir) do
|
|
268
|
-
# Fetch latest
|
|
269
329
|
run_git(%w[fetch origin])
|
|
330
|
+
end
|
|
270
331
|
|
|
271
|
-
|
|
272
|
-
|
|
332
|
+
# Create worktree
|
|
333
|
+
result = Aidp::Worktree.create(
|
|
334
|
+
slug: slug,
|
|
335
|
+
project_dir: @project_dir,
|
|
336
|
+
branch: head_ref,
|
|
337
|
+
base_branch: nil # Branch already exists, no base needed
|
|
338
|
+
)
|
|
273
339
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
340
|
+
worktree_path = result[:path]
|
|
341
|
+
Aidp.log_debug("ci_fix_processor", "worktree_created", pr_number: pr_number, branch: head_ref, path: worktree_path)
|
|
342
|
+
display_message("✅ Worktree created at #{worktree_path}", type: :success)
|
|
277
343
|
|
|
278
|
-
|
|
344
|
+
worktree_path
|
|
279
345
|
end
|
|
280
346
|
|
|
281
|
-
def apply_fixes(fixes)
|
|
347
|
+
def apply_fixes(fixes, working_dir:)
|
|
282
348
|
fixes.each do |fix|
|
|
283
|
-
file_path = File.join(
|
|
349
|
+
file_path = File.join(working_dir, fix["file"])
|
|
284
350
|
|
|
285
351
|
case fix["action"]
|
|
286
352
|
when "create", "edit"
|
|
@@ -296,8 +362,8 @@ module Aidp
|
|
|
296
362
|
end
|
|
297
363
|
end
|
|
298
364
|
|
|
299
|
-
def commit_and_push(pr_data, analysis)
|
|
300
|
-
Dir.chdir(
|
|
365
|
+
def commit_and_push(pr_data, analysis, working_dir:)
|
|
366
|
+
Dir.chdir(working_dir) do
|
|
301
367
|
# Check if there are changes
|
|
302
368
|
status_output = run_git(%w[status --porcelain])
|
|
303
369
|
if status_output.strip.empty?
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Aidp
|
|
8
|
+
module Watch
|
|
9
|
+
# Intelligently extracts relevant failure information from CI logs
|
|
10
|
+
# to reduce token usage when analyzing failures with AI.
|
|
11
|
+
#
|
|
12
|
+
# Instead of feeding full CI output to AI, this creates a tailored
|
|
13
|
+
# extraction script for each failure type, runs it, and provides
|
|
14
|
+
# only the relevant excerpts.
|
|
15
|
+
class CiLogExtractor
|
|
16
|
+
# Maximum size for extracted log content (in characters)
|
|
17
|
+
MAX_EXTRACTED_SIZE = 10_000
|
|
18
|
+
|
|
19
|
+
def initialize(provider_manager:)
|
|
20
|
+
@provider_manager = provider_manager
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Extract relevant failure information from a CI check
|
|
24
|
+
#
|
|
25
|
+
# @param check [Hash] Failed check information
|
|
26
|
+
# @param check_run_url [String] URL to the GitHub Actions run (optional)
|
|
27
|
+
# @return [Hash] Extracted failure info with :summary, :details, :script_used
|
|
28
|
+
def extract_failure_info(check:, check_run_url: nil)
|
|
29
|
+
Aidp.log_debug("ci_log_extractor", "extract_start",
|
|
30
|
+
check_name: check[:name],
|
|
31
|
+
has_output: !check[:output].nil?)
|
|
32
|
+
|
|
33
|
+
# If we have structured output from the check, use it
|
|
34
|
+
if check[:output] && check[:output]["summary"]
|
|
35
|
+
return extract_from_structured_output(check)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# If we have a check run URL, fetch the logs
|
|
39
|
+
if check_run_url
|
|
40
|
+
raw_logs = fetch_check_logs(check_run_url)
|
|
41
|
+
return extract_from_raw_logs(check: check, raw_logs: raw_logs) if raw_logs
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Fallback: minimal information
|
|
45
|
+
{
|
|
46
|
+
summary: "Check '#{check[:name]}' failed",
|
|
47
|
+
details: check[:output]&.dig("text") || "No additional details available",
|
|
48
|
+
extraction_method: "fallback"
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def extract_from_structured_output(check)
|
|
55
|
+
summary = check[:output]["summary"] || ""
|
|
56
|
+
text = check[:output]["text"] || ""
|
|
57
|
+
|
|
58
|
+
# If the output is already concise, return it directly
|
|
59
|
+
full_output = "#{summary}\n\n#{text}".strip
|
|
60
|
+
if full_output.length <= MAX_EXTRACTED_SIZE
|
|
61
|
+
return {
|
|
62
|
+
summary: summary,
|
|
63
|
+
details: text,
|
|
64
|
+
extraction_method: "structured"
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Output is too large, create extraction script
|
|
69
|
+
extraction_result = create_and_run_extraction_script(
|
|
70
|
+
check_name: check[:name],
|
|
71
|
+
raw_content: full_output
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
summary: summary,
|
|
76
|
+
details: extraction_result[:extracted_content],
|
|
77
|
+
extraction_method: "ai_script",
|
|
78
|
+
script_used: extraction_result[:script]
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def extract_from_raw_logs(check:, raw_logs:)
|
|
83
|
+
# If logs are already concise, return them
|
|
84
|
+
if raw_logs.length <= MAX_EXTRACTED_SIZE
|
|
85
|
+
return {
|
|
86
|
+
summary: "Check '#{check[:name]}' failed",
|
|
87
|
+
details: raw_logs,
|
|
88
|
+
extraction_method: "raw"
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Logs are too large, create extraction script
|
|
93
|
+
extraction_result = create_and_run_extraction_script(
|
|
94
|
+
check_name: check[:name],
|
|
95
|
+
raw_content: raw_logs
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
{
|
|
99
|
+
summary: "Check '#{check[:name]}' failed",
|
|
100
|
+
details: extraction_result[:extracted_content],
|
|
101
|
+
extraction_method: "ai_script",
|
|
102
|
+
script_used: extraction_result[:script]
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def create_and_run_extraction_script(check_name:, raw_content:)
|
|
107
|
+
Aidp.log_debug("ci_log_extractor", "creating_script",
|
|
108
|
+
check_name: check_name,
|
|
109
|
+
content_size: raw_content.length)
|
|
110
|
+
|
|
111
|
+
# Ask AI to create an extraction script
|
|
112
|
+
script = generate_extraction_script(
|
|
113
|
+
check_name: check_name,
|
|
114
|
+
sample_content: truncate_sample(raw_content)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Run the script
|
|
118
|
+
extracted = run_extraction_script(script: script, input: raw_content)
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
extracted_content: extracted,
|
|
122
|
+
script: script
|
|
123
|
+
}
|
|
124
|
+
rescue => e
|
|
125
|
+
Aidp.log_warn("ci_log_extractor", "script_execution_failed",
|
|
126
|
+
error: e.message,
|
|
127
|
+
check_name: check_name)
|
|
128
|
+
|
|
129
|
+
# Fallback: simple head/tail extraction
|
|
130
|
+
{
|
|
131
|
+
extracted_content: simple_extract(raw_content),
|
|
132
|
+
script: nil
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def generate_extraction_script(check_name:, sample_content:)
|
|
137
|
+
prompt = <<~PROMPT
|
|
138
|
+
Create a shell script that extracts relevant failure information from CI logs.
|
|
139
|
+
|
|
140
|
+
The script should:
|
|
141
|
+
1. Read from STDIN
|
|
142
|
+
2. Extract ONLY the relevant error messages, failed tests, and stack traces
|
|
143
|
+
3. Omit verbose output, successful tests, and build information
|
|
144
|
+
4. Keep the extracted output under #{MAX_EXTRACTED_SIZE} characters
|
|
145
|
+
5. Output the extracted content to STDOUT
|
|
146
|
+
|
|
147
|
+
Check type: #{check_name}
|
|
148
|
+
|
|
149
|
+
Sample of the log content (first ~2000 chars):
|
|
150
|
+
```
|
|
151
|
+
#{sample_content}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Requirements:
|
|
155
|
+
- Use standard Unix tools (grep, awk, sed, head, tail)
|
|
156
|
+
- Handle multi-line error messages
|
|
157
|
+
- Focus on actionable error information
|
|
158
|
+
- If it's a test failure, extract test names and failure messages
|
|
159
|
+
- If it's a linting error, extract file names, line numbers, and violations
|
|
160
|
+
|
|
161
|
+
Respond ONLY with the shell script code, no explanation.
|
|
162
|
+
The script must be a valid bash script that reads STDIN.
|
|
163
|
+
|
|
164
|
+
Example format:
|
|
165
|
+
#!/bin/bash
|
|
166
|
+
grep -A 5 "FAILED" | head -n 100
|
|
167
|
+
PROMPT
|
|
168
|
+
|
|
169
|
+
response = @provider_manager.send_message(prompt: prompt)
|
|
170
|
+
extract_script_from_response(response.to_s)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def extract_script_from_response(response)
|
|
174
|
+
# Remove markdown code fences if present
|
|
175
|
+
script = response.strip
|
|
176
|
+
script = script.gsub(/^```(?:bash|sh)?\n/, "")
|
|
177
|
+
script = script.gsub(/\n```$/, "")
|
|
178
|
+
|
|
179
|
+
# Ensure it starts with shebang
|
|
180
|
+
unless script.start_with?("#!/")
|
|
181
|
+
script = "#!/bin/bash\n#{script}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
script
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def run_extraction_script(script:, input:)
|
|
188
|
+
Tempfile.create(["ci_extract", ".sh"]) do |script_file|
|
|
189
|
+
script_file.write(script)
|
|
190
|
+
script_file.flush
|
|
191
|
+
script_file.chmod(0o755)
|
|
192
|
+
|
|
193
|
+
stdout, stderr, status = Open3.capture3(
|
|
194
|
+
"bash", script_file.path,
|
|
195
|
+
stdin_data: input,
|
|
196
|
+
chdir: Dir.tmpdir
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
unless status.success?
|
|
200
|
+
Aidp.log_warn("ci_log_extractor", "script_failed",
|
|
201
|
+
exit_code: status.exitstatus,
|
|
202
|
+
stderr: stderr[0, 500])
|
|
203
|
+
return simple_extract(input)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Ensure output is within size limit
|
|
207
|
+
truncate_to_size(stdout)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def fetch_check_logs(check_run_url)
|
|
212
|
+
# TODO: Implement fetching logs from GitHub Actions
|
|
213
|
+
# This would require parsing the check run URL and using gh CLI or API
|
|
214
|
+
# For now, return nil to use structured output
|
|
215
|
+
nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def truncate_sample(content, size: 2000)
|
|
219
|
+
return content if content.length <= size
|
|
220
|
+
"#{content[0, size]}\n... [truncated]"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def simple_extract(content)
|
|
224
|
+
# Simple fallback: extract lines containing error keywords
|
|
225
|
+
lines = content.lines
|
|
226
|
+
error_lines = lines.select do |line|
|
|
227
|
+
line.match?(/error|fail|exception|assert/i)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# If we found error lines, return them with context
|
|
231
|
+
if error_lines.any?
|
|
232
|
+
# Get line numbers of errors and include surrounding context
|
|
233
|
+
result = []
|
|
234
|
+
lines.each_with_index do |line, i|
|
|
235
|
+
if line.match?(/error|fail|exception|assert/i)
|
|
236
|
+
# Include 2 lines before and 3 lines after
|
|
237
|
+
start_idx = [0, i - 2].max
|
|
238
|
+
end_idx = [lines.length - 1, i + 3].min
|
|
239
|
+
result.concat(lines[start_idx..end_idx])
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
truncate_to_size(result.uniq.join)
|
|
244
|
+
else
|
|
245
|
+
# No clear errors, return head and tail
|
|
246
|
+
head = lines.first(50).join
|
|
247
|
+
tail = lines.last(50).join
|
|
248
|
+
truncate_to_size("=== First 50 lines ===\n#{head}\n\n=== Last 50 lines ===\n#{tail}")
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def truncate_to_size(content, size: MAX_EXTRACTED_SIZE)
|
|
253
|
+
return content if content.length <= size
|
|
254
|
+
"#{content[0, size - 20]}\n... [truncated]"
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Watch
|
|
7
|
+
# Extracts state information from GitHub issues/PRs using labels and comments
|
|
8
|
+
# as the single source of truth, enabling multiple AIDP instances to coordinate
|
|
9
|
+
# without local state files.
|
|
10
|
+
class GitHubStateExtractor
|
|
11
|
+
# Pattern for detecting completion comments
|
|
12
|
+
COMPLETION_PATTERN = /✅ Implementation complete for #(\d+)/i
|
|
13
|
+
|
|
14
|
+
# Pattern for detecting detection comments
|
|
15
|
+
DETECTION_PATTERN = /aidp detected `([^`]+)` at ([^\n]+) and is working on it/i
|
|
16
|
+
|
|
17
|
+
# Pattern for detecting plan proposal comments
|
|
18
|
+
PLAN_PROPOSAL_PATTERN = /<!-- PLAN_SUMMARY_START -->/i
|
|
19
|
+
|
|
20
|
+
# Pattern for detecting in-progress label
|
|
21
|
+
IN_PROGRESS_LABEL = "aidp-in-progress"
|
|
22
|
+
|
|
23
|
+
def initialize(repository_client:)
|
|
24
|
+
@repository_client = repository_client
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Check if an issue/PR is currently being worked on by another instance
|
|
28
|
+
def in_progress?(item)
|
|
29
|
+
has_label?(item, IN_PROGRESS_LABEL)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Check if build is already completed for an issue
|
|
33
|
+
# Looks for completion comment from AIDP
|
|
34
|
+
def build_completed?(issue)
|
|
35
|
+
return false unless issue[:comments]
|
|
36
|
+
|
|
37
|
+
issue[:comments].any? do |comment|
|
|
38
|
+
comment["body"]&.match?(COMPLETION_PATTERN)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Check if a plan has been posted for an issue
|
|
43
|
+
def plan_posted?(issue)
|
|
44
|
+
return false unless issue[:comments]
|
|
45
|
+
|
|
46
|
+
issue[:comments].any? do |comment|
|
|
47
|
+
comment["body"]&.match?(PLAN_PROPOSAL_PATTERN)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Extract the most recent plan data from comments
|
|
52
|
+
def extract_plan_data(issue)
|
|
53
|
+
return nil unless issue[:comments]
|
|
54
|
+
|
|
55
|
+
# Find the most recent plan proposal comment
|
|
56
|
+
plan_comment = issue[:comments].reverse.find do |comment|
|
|
57
|
+
comment["body"]&.match?(PLAN_PROPOSAL_PATTERN)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
return nil unless plan_comment
|
|
61
|
+
|
|
62
|
+
body = plan_comment["body"]
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
summary: extract_section(body, "PLAN_SUMMARY"),
|
|
66
|
+
tasks: extract_tasks(body),
|
|
67
|
+
questions: extract_questions(body),
|
|
68
|
+
comment_body: body,
|
|
69
|
+
comment_hint: "## 🤖 AIDP Plan Proposal",
|
|
70
|
+
comment_id: plan_comment["id"],
|
|
71
|
+
posted_at: plan_comment["createdAt"] || Time.now.utc.iso8601
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if detection comment was already posted for this label
|
|
76
|
+
def detection_comment_posted?(item, label)
|
|
77
|
+
return false unless item[:comments]
|
|
78
|
+
|
|
79
|
+
item[:comments].any? do |comment|
|
|
80
|
+
next unless comment["body"]
|
|
81
|
+
|
|
82
|
+
match = comment["body"].match(DETECTION_PATTERN)
|
|
83
|
+
match && match[1] == label
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check if review has been completed for a PR
|
|
88
|
+
def review_completed?(pr)
|
|
89
|
+
return false unless pr[:comments]
|
|
90
|
+
|
|
91
|
+
pr[:comments].any? do |comment|
|
|
92
|
+
comment["body"]&.match?(/🔍.*Review complete/i)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if CI fix has been completed for a PR
|
|
97
|
+
def ci_fix_completed?(pr)
|
|
98
|
+
return false unless pr[:comments]
|
|
99
|
+
|
|
100
|
+
pr[:comments].any? do |comment|
|
|
101
|
+
body = comment["body"]
|
|
102
|
+
next false unless body
|
|
103
|
+
|
|
104
|
+
body.include?("✅") && (
|
|
105
|
+
body.match?(/CI fixes applied/i) ||
|
|
106
|
+
(body.match?(/CI check/i) && body.match?(/passed/i))
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Check if change request has been processed for a PR
|
|
112
|
+
def change_request_processed?(pr)
|
|
113
|
+
return false unless pr[:comments]
|
|
114
|
+
|
|
115
|
+
pr[:comments].any? do |comment|
|
|
116
|
+
body = comment["body"]
|
|
117
|
+
next false unless body
|
|
118
|
+
|
|
119
|
+
body.include?("✅") && body.match?(/Change requests? (?:addressed|applied|complete)/i)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Extract linked issue number from PR description
|
|
124
|
+
# Looks for patterns like "Fixes #123", "Closes #456", "Resolves #789"
|
|
125
|
+
# Returns the issue number as an integer, or nil if not found
|
|
126
|
+
def extract_linked_issue(pr_body)
|
|
127
|
+
return nil unless pr_body
|
|
128
|
+
|
|
129
|
+
# Match common GitHub issue linking patterns
|
|
130
|
+
# https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue
|
|
131
|
+
match = pr_body.match(/(?:Fixes|Closes|Resolves|Close|Fix|Resolve)\s+#(\d+)/i)
|
|
132
|
+
match ? match[1].to_i : nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def has_label?(item, label_name)
|
|
138
|
+
Array(item[:labels]).any? do |label|
|
|
139
|
+
name = label.is_a?(Hash) ? label["name"] : label.to_s
|
|
140
|
+
name.casecmp?(label_name)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def extract_section(body, section_name)
|
|
145
|
+
start_marker = "<!-- #{section_name}_START -->"
|
|
146
|
+
end_marker = "<!-- #{section_name}_END -->"
|
|
147
|
+
|
|
148
|
+
start_idx = body.index(start_marker)
|
|
149
|
+
end_idx = body.index(end_marker)
|
|
150
|
+
|
|
151
|
+
return nil unless start_idx && end_idx
|
|
152
|
+
|
|
153
|
+
content = body[(start_idx + start_marker.length)...end_idx]
|
|
154
|
+
|
|
155
|
+
# Remove markdown heading if present (e.g., "## Heading\n" or "### Heading\n")
|
|
156
|
+
# Strip lines starting with ## or ### to avoid ReDoS
|
|
157
|
+
content.lines.reject { |line| line.start_with?("##", "###") }.join.strip
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def extract_tasks(body)
|
|
161
|
+
tasks_section = extract_section(body, "PLAN_TASKS")
|
|
162
|
+
return [] unless tasks_section
|
|
163
|
+
|
|
164
|
+
# Extract markdown list items (- followed by space and content)
|
|
165
|
+
tasks_section.scan(/^- (.+)$/).flatten
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def extract_questions(body)
|
|
169
|
+
questions_section = extract_section(body, "CLARIFYING_QUESTIONS")
|
|
170
|
+
return [] unless questions_section
|
|
171
|
+
|
|
172
|
+
# Extract numbered list items (number, dot, space, content)
|
|
173
|
+
questions_section.scan(/^\d+\. (.+)$/).flatten
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|