aidp 0.33.0 → 0.34.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
  4. data/lib/aidp/cli/eval_command.rb +399 -0
  5. data/lib/aidp/cli/harness_command.rb +1 -1
  6. data/lib/aidp/cli/security_command.rb +416 -0
  7. data/lib/aidp/cli/tools_command.rb +6 -4
  8. data/lib/aidp/cli.rb +170 -3
  9. data/lib/aidp/concurrency/exec.rb +3 -0
  10. data/lib/aidp/config.rb +113 -0
  11. data/lib/aidp/config_paths.rb +20 -0
  12. data/lib/aidp/daemon/runner.rb +8 -4
  13. data/lib/aidp/errors.rb +134 -0
  14. data/lib/aidp/evaluations/context_capture.rb +205 -0
  15. data/lib/aidp/evaluations/evaluation_record.rb +114 -0
  16. data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
  17. data/lib/aidp/evaluations.rb +23 -0
  18. data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
  19. data/lib/aidp/execute/interactive_repl.rb +6 -2
  20. data/lib/aidp/execute/prompt_evaluator.rb +359 -0
  21. data/lib/aidp/execute/repl_macros.rb +100 -1
  22. data/lib/aidp/execute/work_loop_runner.rb +399 -47
  23. data/lib/aidp/execute/work_loop_state.rb +4 -1
  24. data/lib/aidp/execute/workflow_selector.rb +3 -0
  25. data/lib/aidp/harness/ai_decision_engine.rb +79 -0
  26. data/lib/aidp/harness/capability_registry.rb +2 -0
  27. data/lib/aidp/harness/condition_detector.rb +3 -0
  28. data/lib/aidp/harness/config_loader.rb +3 -0
  29. data/lib/aidp/harness/enhanced_runner.rb +14 -11
  30. data/lib/aidp/harness/error_handler.rb +3 -0
  31. data/lib/aidp/harness/provider_factory.rb +3 -0
  32. data/lib/aidp/harness/provider_manager.rb +6 -0
  33. data/lib/aidp/harness/runner.rb +5 -1
  34. data/lib/aidp/harness/state/persistence.rb +3 -0
  35. data/lib/aidp/harness/state_manager.rb +3 -0
  36. data/lib/aidp/harness/status_display.rb +28 -20
  37. data/lib/aidp/harness/thinking_depth_manager.rb +32 -32
  38. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
  39. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
  40. data/lib/aidp/harness/ui/error_handler.rb +3 -0
  41. data/lib/aidp/harness/ui/job_monitor.rb +4 -0
  42. data/lib/aidp/harness/ui/navigation/submenu.rb +2 -0
  43. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
  44. data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
  45. data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
  46. data/lib/aidp/harness/user_interface.rb +3 -0
  47. data/lib/aidp/loader.rb +2 -2
  48. data/lib/aidp/logger.rb +3 -0
  49. data/lib/aidp/message_display.rb +31 -0
  50. data/lib/aidp/pr_worktree_manager.rb +18 -6
  51. data/lib/aidp/provider_manager.rb +3 -0
  52. data/lib/aidp/providers/base.rb +2 -0
  53. data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
  54. data/lib/aidp/security/secrets_proxy.rb +328 -0
  55. data/lib/aidp/security/secrets_registry.rb +227 -0
  56. data/lib/aidp/security/trifecta_state.rb +220 -0
  57. data/lib/aidp/security/watch_mode_handler.rb +306 -0
  58. data/lib/aidp/security/work_loop_adapter.rb +277 -0
  59. data/lib/aidp/security.rb +56 -0
  60. data/lib/aidp/setup/wizard.rb +4 -2
  61. data/lib/aidp/version.rb +1 -1
  62. data/lib/aidp/watch/auto_merger.rb +274 -0
  63. data/lib/aidp/watch/auto_pr_processor.rb +125 -7
  64. data/lib/aidp/watch/build_processor.rb +16 -1
  65. data/lib/aidp/watch/change_request_processor.rb +680 -286
  66. data/lib/aidp/watch/ci_fix_processor.rb +262 -4
  67. data/lib/aidp/watch/feedback_collector.rb +191 -0
  68. data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
  69. data/lib/aidp/watch/implementation_verifier.rb +142 -1
  70. data/lib/aidp/watch/plan_generator.rb +70 -13
  71. data/lib/aidp/watch/plan_processor.rb +12 -5
  72. data/lib/aidp/watch/projects_processor.rb +286 -0
  73. data/lib/aidp/watch/repository_client.rb +861 -53
  74. data/lib/aidp/watch/review_processor.rb +33 -6
  75. data/lib/aidp/watch/runner.rb +51 -11
  76. data/lib/aidp/watch/state_store.rb +233 -0
  77. data/lib/aidp/watch/sub_issue_creator.rb +221 -0
  78. data/lib/aidp/workflows/guided_agent.rb +4 -0
  79. data/lib/aidp/workstream_executor.rb +3 -0
  80. data/lib/aidp/worktree.rb +61 -11
  81. data/lib/aidp/worktree_branch_manager.rb +347 -101
  82. data/templates/implementation/iterative_implementation.md +46 -3
  83. metadata +20 -1
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../message_display"
4
+
5
+ module Aidp
6
+ module Watch
7
+ # Handles hierarchical PR creation strategy for parent and sub-issues.
8
+ # Parent issues get draft PRs targeting main. Sub-issues get PRs targeting
9
+ # the parent's branch.
10
+ class HierarchicalPrStrategy
11
+ include Aidp::MessageDisplay
12
+
13
+ # Labels for hierarchical PR identification
14
+ PARENT_PR_LABEL = "aidp-parent-pr"
15
+ SUB_PR_LABEL = "aidp-sub-pr"
16
+
17
+ attr_reader :repository_client, :state_store
18
+
19
+ def initialize(repository_client:, state_store:)
20
+ @repository_client = repository_client
21
+ @state_store = state_store
22
+ end
23
+
24
+ # Determine if an issue is a parent issue (has sub-issues)
25
+ # @param issue_number [Integer] The issue number
26
+ # @return [Boolean] True if this is a parent issue
27
+ def parent_issue?(issue_number)
28
+ sub_issues = @state_store.sub_issues(issue_number)
29
+ sub_issues.any?
30
+ end
31
+
32
+ # Determine if an issue is a sub-issue (has a parent)
33
+ # @param issue_number [Integer] The issue number
34
+ # @return [Boolean] True if this is a sub-issue
35
+ def sub_issue?(issue_number)
36
+ parent = @state_store.parent_issue(issue_number)
37
+ !parent.nil?
38
+ end
39
+
40
+ # Get PR creation options for an issue based on its hierarchy position
41
+ # @param issue [Hash] The issue data
42
+ # @param default_base_branch [String] The default base branch (e.g., "main")
43
+ # @return [Hash] PR options including :base_branch, :draft, :labels
44
+ def pr_options_for_issue(issue, default_base_branch:)
45
+ issue_number = issue[:number]
46
+ Aidp.log_debug("hierarchical_pr_strategy", "determining_pr_options",
47
+ issue_number: issue_number, default_base: default_base_branch)
48
+
49
+ if parent_issue?(issue_number)
50
+ parent_pr_options(issue, default_base_branch)
51
+ elsif sub_issue?(issue_number)
52
+ sub_issue_pr_options(issue, default_base_branch)
53
+ else
54
+ # Regular issue - use default behavior
55
+ regular_pr_options(issue, default_base_branch)
56
+ end
57
+ end
58
+
59
+ # Generate branch name for hierarchical issues
60
+ # @param issue [Hash] The issue data
61
+ # @return [String] The branch name
62
+ def branch_name_for(issue)
63
+ issue_number = issue[:number]
64
+ slug = issue_slug(issue)
65
+
66
+ if parent_issue?(issue_number)
67
+ # Parent branch: aidp/parent-{number}-{slug}
68
+ "aidp/parent-#{issue_number}-#{slug}"
69
+ elsif sub_issue?(issue_number)
70
+ parent_number = @state_store.parent_issue(issue_number)
71
+ # Sub-issue branch: aidp/sub-{parent}-{number}-{slug}
72
+ "aidp/sub-#{parent_number}-#{issue_number}-#{slug}"
73
+ else
74
+ # Regular branch: aidp/issue-{number}-{slug}
75
+ "aidp/issue-#{issue_number}-#{slug}"
76
+ end
77
+ end
78
+
79
+ # Build PR description with hierarchy context
80
+ # @param issue [Hash] The issue data
81
+ # @param plan_summary [String] The plan summary
82
+ # @return [String] The PR description
83
+ def pr_description_for(issue, plan_summary:)
84
+ issue_number = issue[:number]
85
+
86
+ if parent_issue?(issue_number)
87
+ build_parent_pr_description(issue, plan_summary)
88
+ elsif sub_issue?(issue_number)
89
+ build_sub_issue_pr_description(issue, plan_summary)
90
+ else
91
+ build_regular_pr_description(issue, plan_summary)
92
+ end
93
+ end
94
+
95
+ # Get the base branch for a sub-issue PR (the parent's branch)
96
+ # @param issue_number [Integer] The sub-issue number
97
+ # @return [String, nil] The parent's branch name, or nil if not found
98
+ def parent_branch_for_sub_issue(issue_number)
99
+ parent_number = @state_store.parent_issue(issue_number)
100
+ return nil unless parent_number
101
+
102
+ parent_build = @state_store.workstream_for_issue(parent_number)
103
+ return nil unless parent_build
104
+
105
+ parent_build[:branch]
106
+ end
107
+
108
+ private
109
+
110
+ def issue_slug(issue)
111
+ issue[:title].to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")[0, 32]
112
+ end
113
+
114
+ def parent_pr_options(issue, default_base_branch)
115
+ Aidp.log_debug("hierarchical_pr_strategy", "parent_pr_options",
116
+ issue_number: issue[:number])
117
+
118
+ {
119
+ base_branch: default_base_branch,
120
+ draft: true, # Parent PRs always start as draft
121
+ labels: [PARENT_PR_LABEL],
122
+ additional_context: build_parent_context(issue)
123
+ }
124
+ end
125
+
126
+ def sub_issue_pr_options(issue, default_base_branch)
127
+ issue_number = issue[:number]
128
+ parent_branch = parent_branch_for_sub_issue(issue_number)
129
+
130
+ Aidp.log_debug("hierarchical_pr_strategy", "sub_issue_pr_options",
131
+ issue_number: issue_number, parent_branch: parent_branch)
132
+
133
+ # If parent branch exists, target it; otherwise fall back to default
134
+ base = parent_branch || default_base_branch
135
+
136
+ {
137
+ base_branch: base,
138
+ draft: false, # Sub-PRs can be non-draft (will be auto-merged)
139
+ labels: [SUB_PR_LABEL],
140
+ additional_context: build_sub_issue_context(issue)
141
+ }
142
+ end
143
+
144
+ def regular_pr_options(issue, default_base_branch)
145
+ {
146
+ base_branch: default_base_branch,
147
+ draft: true,
148
+ labels: [],
149
+ additional_context: nil
150
+ }
151
+ end
152
+
153
+ def build_parent_context(issue)
154
+ issue_number = issue[:number]
155
+ sub_issues = @state_store.sub_issues(issue_number)
156
+
157
+ return nil if sub_issues.empty?
158
+
159
+ lines = []
160
+ lines << "### Sub-Issues"
161
+ lines << ""
162
+ lines << "This parent PR aggregates the following sub-issues:"
163
+ lines << ""
164
+
165
+ sub_issues.each do |sub_number|
166
+ sub_build = @state_store.workstream_for_issue(sub_number)
167
+ pr_link = sub_build&.dig(:pr_url) ? sub_build[:pr_url] : "_(PR pending)_"
168
+ lines << "- ##{sub_number}: #{pr_link}"
169
+ end
170
+
171
+ lines << ""
172
+ lines << "**Note:** This PR will be ready for review once all sub-issue PRs are merged."
173
+ lines.join("\n")
174
+ end
175
+
176
+ def build_sub_issue_context(issue)
177
+ issue_number = issue[:number]
178
+ parent_number = @state_store.parent_issue(issue_number)
179
+
180
+ return nil unless parent_number
181
+
182
+ parent_build = @state_store.workstream_for_issue(parent_number)
183
+
184
+ lines = []
185
+ lines << "### Parent Issue"
186
+ lines << ""
187
+ lines << "This PR implements a sub-issue of ##{parent_number}."
188
+ lines << ""
189
+
190
+ pr_link = parent_build&.dig(:pr_url) || "_(pending)_"
191
+ lines << "**Parent PR:** #{pr_link}"
192
+
193
+ lines << ""
194
+ lines << "This sub-issue PR will be automatically merged when CI passes."
195
+ lines.join("\n")
196
+ end
197
+
198
+ def build_parent_pr_description(issue, plan_summary)
199
+ issue_number = issue[:number]
200
+ sub_context = build_parent_context(issue)
201
+
202
+ parts = []
203
+ parts << "Implements ##{issue_number}"
204
+ parts << ""
205
+ parts << "## Summary"
206
+ parts << plan_summary
207
+ parts << ""
208
+
209
+ if sub_context
210
+ parts << sub_context
211
+ parts << ""
212
+ end
213
+
214
+ parts << "---"
215
+ parts << "_This is a parent PR that aggregates changes from sub-issue PRs._"
216
+ parts << "_It requires human review before merging to the main branch._"
217
+
218
+ parts.join("\n")
219
+ end
220
+
221
+ def build_sub_issue_pr_description(issue, plan_summary)
222
+ issue_number = issue[:number]
223
+ sub_context = build_sub_issue_context(issue)
224
+
225
+ parts = []
226
+ parts << "Fixes ##{issue_number}"
227
+ parts << ""
228
+ parts << "## Summary"
229
+ parts << plan_summary
230
+ parts << ""
231
+
232
+ if sub_context
233
+ parts << sub_context
234
+ parts << ""
235
+ end
236
+
237
+ parts << "---"
238
+ parts << "_This is a sub-issue PR that will be auto-merged when CI passes._"
239
+
240
+ parts.join("\n")
241
+ end
242
+
243
+ def build_regular_pr_description(issue, plan_summary)
244
+ <<~DESCRIPTION
245
+ Fixes ##{issue[:number]}
246
+
247
+ ## Summary
248
+ #{plan_summary}
249
+
250
+ ---
251
+ _Automated implementation by AIDP._
252
+ DESCRIPTION
253
+ end
254
+ end
255
+ end
256
+ end
@@ -19,6 +19,9 @@ module Aidp
19
19
 
20
20
  # Verify implementation against issue requirements
21
21
  # Returns: { verified: true/false, reason: String, missing_items: Array }
22
+ #
23
+ # FIX for issue #391: Enhanced verification to require substantive code changes
24
+ # Rejects implementations that only contain documentation changes
22
25
  def verify(issue:, working_dir:)
23
26
  Aidp.log_debug("implementation_verifier", "starting_verification", issue: issue[:number], working_dir: working_dir)
24
27
 
@@ -28,6 +31,24 @@ module Aidp
28
31
  issue_requirements = extract_issue_requirements(issue)
29
32
  implementation_changes = extract_implementation_changes(working_dir)
30
33
 
34
+ # FIX for issue #391: Check for substantive changes before ZFC verification
35
+ substantive_check = verify_substantive_changes(implementation_changes, working_dir)
36
+ unless substantive_check[:has_substantive_changes]
37
+ Aidp.log_warn(
38
+ "implementation_verifier",
39
+ "no_substantive_changes",
40
+ issue: issue[:number],
41
+ reason: substantive_check[:reason]
42
+ )
43
+
44
+ return {
45
+ verified: false,
46
+ reason: substantive_check[:reason],
47
+ missing_items: ["Substantive code changes required - only documentation/config changes detected"],
48
+ additional_work: ["Implement the actual code changes described in the issue"]
49
+ }
50
+ end
51
+
31
52
  # Use ZFC to verify completeness
32
53
  result = perform_zfc_verification(
33
54
  issue_number: issue[:number],
@@ -78,7 +99,10 @@ module Aidp
78
99
 
79
100
  unless status.success?
80
101
  Aidp.log_warn("implementation_verifier", "git_diff_failed", working_dir: working_dir)
81
- return "Unable to extract changes: git diff failed"
102
+ return {
103
+ diff: "",
104
+ files_changed: "Unable to extract changes: git diff failed"
105
+ }
82
106
  end
83
107
 
84
108
  # Get list of changed files with stats
@@ -119,6 +143,123 @@ module Aidp
119
143
  end || "main"
120
144
  end
121
145
 
146
+ # FIX for issue #391: Verify that implementation includes substantive code changes
147
+ # Rejects changes that only include documentation, config, or non-code files
148
+ # Note: If no files changed, we defer to ZFC verification (which handles empty implementations)
149
+ def verify_substantive_changes(implementation_changes, working_dir)
150
+ files_changed = implementation_changes[:files_changed] || ""
151
+ diff = implementation_changes[:diff] || ""
152
+
153
+ # Extract file names from the files changed summary
154
+ changed_files = extract_changed_file_names(files_changed)
155
+
156
+ Aidp.log_debug("implementation_verifier", "checking_substantive_changes",
157
+ total_files: changed_files.size,
158
+ files: changed_files.take(10))
159
+
160
+ # No changes at all - defer to ZFC verification for proper handling
161
+ # ZFC will determine if an empty implementation is valid for the issue
162
+ if changed_files.empty?
163
+ Aidp.log_debug("implementation_verifier", "no_files_changed_deferring_to_zfc")
164
+ return {
165
+ has_substantive_changes: true, # Allow ZFC to make the determination
166
+ reason: "No files changed - deferring to ZFC verification"
167
+ }
168
+ end
169
+
170
+ # Categorize files
171
+ code_files = []
172
+ test_files = []
173
+ doc_files = []
174
+ config_files = []
175
+ other_files = []
176
+
177
+ changed_files.each do |file|
178
+ case file
179
+ when /\.(rb|py|js|ts|jsx|tsx|go|rs|java|kt|swift|c|cpp|h|hpp|cs)$/i
180
+ if /(_spec|_test|\.spec|\.test|\/spec\/|\/test\/)/i.match?(file)
181
+ test_files << file
182
+ else
183
+ code_files << file
184
+ end
185
+ # Only clearly documentation files - not .txt which could be anything
186
+ when /\.(md|rst|adoc|rdoc)$/i, /^README/i, /^CHANGELOG/i, /^LICENSE/i
187
+ doc_files << file
188
+ when /\.(yml|yaml|json|toml|ini|env|config)$/i, /\.gitignore$/, /Gemfile/, /package\.json/
189
+ config_files << file
190
+ else
191
+ other_files << file
192
+ end
193
+ end
194
+
195
+ Aidp.log_debug("implementation_verifier", "file_categorization",
196
+ code_files: code_files.size,
197
+ test_files: test_files.size,
198
+ doc_files: doc_files.size,
199
+ config_files: config_files.size,
200
+ other_files: other_files.size)
201
+
202
+ # Check if there are substantive code changes
203
+ # Substantive means: actual code files changed, not just docs/config
204
+ # Note: "other" files (unknown extensions) are allowed through to ZFC for proper evaluation
205
+ if code_files.empty? && test_files.empty? && other_files.empty?
206
+ if doc_files.any? && config_files.empty?
207
+ return {
208
+ has_substantive_changes: false,
209
+ reason: "Only documentation files were changed (#{doc_files.join(", ")}). " \
210
+ "Implementation requires code changes."
211
+ }
212
+ elsif config_files.any? && doc_files.empty?
213
+ return {
214
+ has_substantive_changes: false,
215
+ reason: "Only configuration files were changed (#{config_files.join(", ")}). " \
216
+ "Implementation requires code changes."
217
+ }
218
+ elsif doc_files.any? || config_files.any?
219
+ return {
220
+ has_substantive_changes: false,
221
+ reason: "Only documentation and configuration files were changed. " \
222
+ "Implementation requires code changes."
223
+ }
224
+ end
225
+ end
226
+
227
+ # If only test files changed, that's potentially valid for test-related issues
228
+ # but we should flag it for issues that require implementation
229
+ if code_files.empty? && test_files.any?
230
+ # This is acceptable but worth noting
231
+ Aidp.log_debug("implementation_verifier", "only_test_files_changed",
232
+ test_files: test_files)
233
+ end
234
+
235
+ # Check diff size - very small diffs might be insignificant
236
+ if diff.bytesize < 100 && code_files.any?
237
+ return {
238
+ has_substantive_changes: false,
239
+ reason: "Code changes are too minimal (#{diff.bytesize} bytes). " \
240
+ "Please implement the required functionality fully."
241
+ }
242
+ end
243
+
244
+ {
245
+ has_substantive_changes: true,
246
+ reason: "Found #{code_files.size} code files and #{test_files.size} test files changed"
247
+ }
248
+ end
249
+
250
+ def extract_changed_file_names(files_changed_summary)
251
+ return [] if files_changed_summary.nil? || files_changed_summary.empty?
252
+
253
+ # Parse git diff --stat output format:
254
+ # lib/aidp/foo.rb | 10 +++++-----
255
+ # docs/README.md | 3 +++
256
+ files_changed_summary.lines.map do |line|
257
+ # Extract filename from diff --stat format
258
+ match = line.match(/^\s*([^\s|]+)\s*\|/)
259
+ match ? match[1].strip : nil
260
+ end.compact.reject(&:empty?)
261
+ end
262
+
122
263
  def perform_zfc_verification(issue_number:, issue_requirements:, implementation_changes:)
123
264
  # Check if AI decision engine is available
124
265
  unless @ai_decision_engine
@@ -26,13 +26,50 @@ module Aidp
26
26
  Focus on concrete engineering tasks. Ensure questions are actionable.
27
27
  PROMPT
28
28
 
29
- def initialize(provider_name: nil, verbose: false)
29
+ HIERARCHICAL_PROVIDER_PROMPT = <<~PROMPT
30
+ You are AIDP's planning specialist for large projects. Read the GitHub issue and existing comments.
31
+ This is a LARGE project that should be broken down into independent sub-issues.
32
+
33
+ Analyze the requirements and break them into logical sub-tasks that can be worked on independently.
34
+ For each sub-task, identify:
35
+ 1. What should be built (clear, focused scope)
36
+ 2. What skills are required (e.g., "GraphQL API", "React Components", "Database Schema")
37
+ 3. What personas should work on it (e.g., "Backend Engineer", "Frontend Developer")
38
+ 4. What dependencies exist (which other sub-tasks must complete first)
39
+
40
+ Respond in JSON with the following shape (no extra text, no code fences):
41
+ {
42
+ "plan_summary": "one paragraph summary of the overall project",
43
+ "should_create_sub_issues": true,
44
+ "sub_issues": [
45
+ {
46
+ "title": "Brief title for the sub-issue",
47
+ "description": "Detailed description of what needs to be built",
48
+ "tasks": ["Specific task 1", "Specific task 2"],
49
+ "skills": ["Skill 1", "Skill 2"],
50
+ "personas": ["Persona 1"],
51
+ "dependencies": ["Reference to other sub-issue by title if needed"]
52
+ }
53
+ ],
54
+ "clarifying_questions": ["question 1", "question 2"]
55
+ }
56
+
57
+ Guidelines:
58
+ - Each sub-issue should be independently testable and deliverable
59
+ - Sub-issues should be sized for 1-3 days of work
60
+ - Include 3-8 sub-issues for a large project
61
+ - Be specific about skills needed (avoid generic terms)
62
+ - Only create sub-issues if the project truly warrants it (complexity, multiple components, etc.)
63
+ PROMPT
64
+
65
+ def initialize(provider_name: nil, verbose: false, hierarchical: false)
30
66
  @provider_name = provider_name
31
67
  @verbose = verbose
68
+ @hierarchical = hierarchical
32
69
  @providers_attempted = []
33
70
  end
34
71
 
35
- def generate(issue)
72
+ def generate(issue, hierarchical: @hierarchical)
36
73
  Aidp.log_debug("plan_generator", "generate.start", provider: @provider_name, issue: issue[:number])
37
74
 
38
75
  # Try providers in fallback chain order
@@ -52,8 +89,8 @@ module Aidp
52
89
  end
53
90
 
54
91
  begin
55
- Aidp.log_info("plan_generator", "generate_with_provider", provider: provider_name, issue: issue[:number])
56
- result = generate_with_provider(provider, issue, provider_name)
92
+ Aidp.log_info("plan_generator", "generate_with_provider", provider: provider_name, issue: issue[:number], hierarchical: hierarchical)
93
+ result = generate_with_provider(provider, issue, provider_name, hierarchical: hierarchical)
57
94
  if result
58
95
  Aidp.log_info("plan_generator", "generation_success", provider: provider_name, issue: issue[:number])
59
96
  return result
@@ -137,10 +174,10 @@ module Aidp
137
174
  "cursor"
138
175
  end
139
176
 
140
- def generate_with_provider(provider, issue, provider_name = "unknown")
141
- payload = build_prompt(issue)
177
+ def generate_with_provider(provider, issue, provider_name = "unknown", hierarchical: false)
178
+ payload = build_prompt(issue, hierarchical: hierarchical)
142
179
 
143
- Aidp.log_debug("plan_generator", "sending_to_provider", provider: provider_name, prompt_length: payload.length)
180
+ Aidp.log_debug("plan_generator", "sending_to_provider", provider: provider_name, prompt_length: payload.length, hierarchical: hierarchical)
144
181
 
145
182
  if @verbose
146
183
  display_message("\n--- Plan Generation Prompt ---", type: :muted)
@@ -158,10 +195,10 @@ module Aidp
158
195
  display_message("--- End Response ---\n", type: :muted)
159
196
  end
160
197
 
161
- parsed = parse_structured_response(response)
198
+ parsed = parse_structured_response(response, hierarchical: hierarchical)
162
199
 
163
200
  if parsed
164
- Aidp.log_debug("plan_generator", "response_parsed", provider: provider_name, has_summary: !parsed[:summary].to_s.empty?, tasks_count: parsed[:tasks]&.size || 0)
201
+ Aidp.log_debug("plan_generator", "response_parsed", provider: provider_name, has_summary: !parsed[:summary].to_s.empty?, tasks_count: parsed[:tasks]&.size || 0, sub_issues_count: parsed[:sub_issues]&.size || 0)
165
202
  return parsed
166
203
  end
167
204
 
@@ -170,7 +207,7 @@ module Aidp
170
207
  nil
171
208
  end
172
209
 
173
- def build_prompt(issue)
210
+ def build_prompt(issue, hierarchical: false)
174
211
  comments_text = issue[:comments]
175
212
  .sort_by { |comment| comment["createdAt"].to_s }
176
213
  .map do |comment|
@@ -180,8 +217,10 @@ module Aidp
180
217
  end
181
218
  .join("\n\n")
182
219
 
220
+ prompt_template = hierarchical ? HIERARCHICAL_PROVIDER_PROMPT : PROVIDER_PROMPT
221
+
183
222
  <<~PROMPT
184
- #{PROVIDER_PROMPT}
223
+ #{prompt_template}
185
224
 
186
225
  Issue Title: #{issue[:title]}
187
226
  Issue URL: #{issue[:url]}
@@ -194,17 +233,35 @@ module Aidp
194
233
  PROMPT
195
234
  end
196
235
 
197
- def parse_structured_response(response)
236
+ def parse_structured_response(response, hierarchical: false)
198
237
  text = response.to_s.strip
199
238
  candidate = extract_json_payload(text)
200
239
  return nil unless candidate
201
240
 
202
241
  data = JSON.parse(candidate)
203
- {
242
+
243
+ result = {
204
244
  summary: data["plan_summary"].to_s.strip,
205
245
  tasks: Array(data["plan_tasks"]).map(&:to_s),
206
246
  questions: Array(data["clarifying_questions"]).map(&:to_s)
207
247
  }
248
+
249
+ # Add hierarchical planning data if present
250
+ if hierarchical || data["should_create_sub_issues"]
251
+ result[:should_create_sub_issues] = data["should_create_sub_issues"] || false
252
+ result[:sub_issues] = Array(data["sub_issues"]).map do |sub|
253
+ {
254
+ title: sub["title"].to_s,
255
+ description: sub["description"].to_s,
256
+ tasks: Array(sub["tasks"]).map(&:to_s),
257
+ skills: Array(sub["skills"]).map(&:to_s),
258
+ personas: Array(sub["personas"]).map(&:to_s),
259
+ dependencies: Array(sub["dependencies"]).map(&:to_s)
260
+ }
261
+ end
262
+ end
263
+
264
+ result
208
265
  rescue JSON::ParserError
209
266
  nil
210
267
  end
@@ -3,6 +3,7 @@
3
3
  require_relative "../message_display"
4
4
  require_relative "plan_generator"
5
5
  require_relative "state_store"
6
+ require_relative "feedback_collector"
6
7
 
7
8
  module Aidp
8
9
  module Watch
@@ -59,29 +60,35 @@ module Aidp
59
60
  archived_content = existing_plan ? archive_previous_plan(number, existing_plan) : nil
60
61
 
61
62
  comment_body = build_comment(issue: issue, plan: plan_data, label_actor: label_actor, archived_content: archived_content)
63
+ comment_body_with_feedback = FeedbackCollector.append_feedback_prompt(comment_body)
64
+ comment_id = nil
62
65
 
63
66
  if existing_plan && existing_plan["comment_id"]
64
67
  # Update existing comment
65
- @repository_client.update_comment(existing_plan["comment_id"], comment_body)
68
+ @repository_client.update_comment(existing_plan["comment_id"], comment_body_with_feedback)
69
+ comment_id = existing_plan["comment_id"]
66
70
  display_message("📝 Updated plan comment for issue ##{number}", type: :success)
67
71
  elsif existing_plan
68
72
  # Try to find existing comment by header
69
73
  existing_comment = @repository_client.find_comment(number, COMMENT_HEADER)
70
74
  if existing_comment
71
- @repository_client.update_comment(existing_comment[:id], comment_body)
75
+ @repository_client.update_comment(existing_comment[:id], comment_body_with_feedback)
76
+ comment_id = existing_comment[:id]
72
77
  display_message("📝 Updated plan comment for issue ##{number}", type: :success)
73
- plan_data = plan_data.merge(comment_id: existing_comment[:id])
74
78
  else
75
79
  # Fallback to posting new comment if we can't find the old one
76
- @repository_client.post_comment(number, comment_body)
80
+ result = @repository_client.post_comment(number, comment_body_with_feedback)
81
+ comment_id = result[:id] if result.is_a?(Hash)
77
82
  display_message("💬 Posted new plan comment for issue ##{number}", type: :success)
78
83
  end
79
84
  else
80
85
  # First time planning - post new comment
81
- @repository_client.post_comment(number, comment_body)
86
+ result = @repository_client.post_comment(number, comment_body_with_feedback)
87
+ comment_id = result[:id] if result.is_a?(Hash)
82
88
  display_message("💬 Posted plan comment for issue ##{number}", type: :success)
83
89
  end
84
90
 
91
+ plan_data = plan_data.merge(comment_id: comment_id) if comment_id
85
92
  @state_store.record_plan(number, plan_data.merge(comment_body: comment_body, comment_hint: COMMENT_HEADER))
86
93
 
87
94
  # Update labels: remove plan trigger, add appropriate status label