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
|
@@ -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
|