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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +89 -0
  3. data/lib/aidp/cli/models_command.rb +5 -6
  4. data/lib/aidp/cli.rb +10 -8
  5. data/lib/aidp/config.rb +54 -0
  6. data/lib/aidp/debug_mixin.rb +23 -1
  7. data/lib/aidp/execute/agent_signal_parser.rb +22 -0
  8. data/lib/aidp/execute/repl_macros.rb +2 -2
  9. data/lib/aidp/execute/steps.rb +94 -1
  10. data/lib/aidp/execute/work_loop_runner.rb +209 -17
  11. data/lib/aidp/execute/workflow_selector.rb +2 -25
  12. data/lib/aidp/firewall/provider_requirements_collector.rb +262 -0
  13. data/lib/aidp/harness/ai_decision_engine.rb +35 -2
  14. data/lib/aidp/harness/config_manager.rb +0 -5
  15. data/lib/aidp/harness/config_schema.rb +8 -0
  16. data/lib/aidp/harness/configuration.rb +27 -19
  17. data/lib/aidp/harness/enhanced_runner.rb +1 -4
  18. data/lib/aidp/harness/error_handler.rb +1 -72
  19. data/lib/aidp/harness/provider_factory.rb +11 -2
  20. data/lib/aidp/harness/state_manager.rb +0 -7
  21. data/lib/aidp/harness/thinking_depth_manager.rb +47 -68
  22. data/lib/aidp/harness/ui/enhanced_tui.rb +8 -18
  23. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +0 -18
  24. data/lib/aidp/harness/ui/progress_display.rb +6 -2
  25. data/lib/aidp/harness/user_interface.rb +0 -58
  26. data/lib/aidp/init/runner.rb +7 -2
  27. data/lib/aidp/planning/analyzers/feedback_analyzer.rb +365 -0
  28. data/lib/aidp/planning/builders/agile_plan_builder.rb +387 -0
  29. data/lib/aidp/planning/builders/project_plan_builder.rb +193 -0
  30. data/lib/aidp/planning/generators/gantt_generator.rb +190 -0
  31. data/lib/aidp/planning/generators/iteration_plan_generator.rb +392 -0
  32. data/lib/aidp/planning/generators/legacy_research_planner.rb +473 -0
  33. data/lib/aidp/planning/generators/marketing_report_generator.rb +348 -0
  34. data/lib/aidp/planning/generators/mvp_scope_generator.rb +310 -0
  35. data/lib/aidp/planning/generators/user_test_plan_generator.rb +373 -0
  36. data/lib/aidp/planning/generators/wbs_generator.rb +259 -0
  37. data/lib/aidp/planning/mappers/persona_mapper.rb +163 -0
  38. data/lib/aidp/planning/parsers/document_parser.rb +141 -0
  39. data/lib/aidp/planning/parsers/feedback_data_parser.rb +252 -0
  40. data/lib/aidp/provider_manager.rb +8 -32
  41. data/lib/aidp/providers/aider.rb +264 -0
  42. data/lib/aidp/providers/anthropic.rb +74 -2
  43. data/lib/aidp/providers/base.rb +25 -1
  44. data/lib/aidp/providers/codex.rb +26 -3
  45. data/lib/aidp/providers/cursor.rb +16 -0
  46. data/lib/aidp/providers/gemini.rb +13 -0
  47. data/lib/aidp/providers/github_copilot.rb +17 -0
  48. data/lib/aidp/providers/kilocode.rb +11 -0
  49. data/lib/aidp/providers/opencode.rb +11 -0
  50. data/lib/aidp/setup/wizard.rb +249 -39
  51. data/lib/aidp/version.rb +1 -1
  52. data/lib/aidp/watch/build_processor.rb +211 -30
  53. data/lib/aidp/watch/change_request_processor.rb +128 -14
  54. data/lib/aidp/watch/ci_fix_processor.rb +103 -37
  55. data/lib/aidp/watch/ci_log_extractor.rb +258 -0
  56. data/lib/aidp/watch/github_state_extractor.rb +177 -0
  57. data/lib/aidp/watch/implementation_verifier.rb +284 -0
  58. data/lib/aidp/watch/plan_generator.rb +7 -43
  59. data/lib/aidp/watch/plan_processor.rb +7 -6
  60. data/lib/aidp/watch/repository_client.rb +245 -17
  61. data/lib/aidp/watch/review_processor.rb +98 -17
  62. data/lib/aidp/watch/reviewers/base_reviewer.rb +1 -1
  63. data/lib/aidp/watch/runner.rb +181 -29
  64. data/lib/aidp/watch/state_store.rb +22 -1
  65. data/lib/aidp/workflows/definitions.rb +147 -0
  66. data/lib/aidp/workstream_cleanup.rb +245 -0
  67. data/lib/aidp/worktree.rb +19 -0
  68. data/templates/aidp.yml.example +57 -0
  69. data/templates/implementation/generate_tdd_specs.md +213 -0
  70. data/templates/implementation/iterative_implementation.md +122 -0
  71. data/templates/planning/agile/analyze_feedback.md +183 -0
  72. data/templates/planning/agile/generate_iteration_plan.md +179 -0
  73. data/templates/planning/agile/generate_legacy_research_plan.md +171 -0
  74. data/templates/planning/agile/generate_marketing_report.md +162 -0
  75. data/templates/planning/agile/generate_mvp_scope.md +127 -0
  76. data/templates/planning/agile/generate_user_test_plan.md +143 -0
  77. data/templates/planning/agile/ingest_feedback.md +174 -0
  78. data/templates/planning/assemble_project_plan.md +113 -0
  79. data/templates/planning/assign_personas.md +108 -0
  80. data/templates/planning/create_tasks.md +52 -6
  81. data/templates/planning/generate_gantt.md +86 -0
  82. data/templates/planning/generate_wbs.md +85 -0
  83. data/templates/planning/initialize_planning_mode.md +70 -0
  84. data/templates/skills/README.md +2 -2
  85. data/templates/skills/marketing_strategist/SKILL.md +279 -0
  86. data/templates/skills/product_manager/SKILL.md +177 -0
  87. data/templates/skills/ruby_aidp_planning/SKILL.md +497 -0
  88. data/templates/skills/ruby_rspec_tdd/SKILL.md +514 -0
  89. data/templates/skills/ux_researcher/SKILL.md +222 -0
  90. 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