aidp 0.33.0 → 0.34.1
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 +35 -0
- data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
- data/lib/aidp/cli/eval_command.rb +399 -0
- data/lib/aidp/cli/harness_command.rb +1 -1
- data/lib/aidp/cli/security_command.rb +416 -0
- data/lib/aidp/cli/tools_command.rb +6 -4
- data/lib/aidp/cli.rb +170 -3
- data/lib/aidp/concurrency/exec.rb +3 -0
- data/lib/aidp/config.rb +113 -0
- data/lib/aidp/config_paths.rb +20 -0
- data/lib/aidp/daemon/runner.rb +8 -4
- data/lib/aidp/errors.rb +134 -0
- data/lib/aidp/evaluations/context_capture.rb +205 -0
- data/lib/aidp/evaluations/evaluation_record.rb +114 -0
- data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
- data/lib/aidp/evaluations.rb +23 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
- data/lib/aidp/execute/interactive_repl.rb +6 -2
- data/lib/aidp/execute/prompt_evaluator.rb +359 -0
- data/lib/aidp/execute/repl_macros.rb +100 -1
- data/lib/aidp/execute/work_loop_runner.rb +399 -47
- data/lib/aidp/execute/work_loop_state.rb +4 -1
- data/lib/aidp/execute/workflow_selector.rb +3 -0
- data/lib/aidp/harness/ai_decision_engine.rb +79 -0
- data/lib/aidp/harness/capability_registry.rb +2 -0
- data/lib/aidp/harness/condition_detector.rb +3 -0
- data/lib/aidp/harness/config_loader.rb +3 -0
- data/lib/aidp/harness/enhanced_runner.rb +14 -11
- data/lib/aidp/harness/error_handler.rb +3 -0
- data/lib/aidp/harness/provider_factory.rb +3 -0
- data/lib/aidp/harness/provider_manager.rb +6 -0
- data/lib/aidp/harness/runner.rb +5 -1
- data/lib/aidp/harness/state/persistence.rb +3 -0
- data/lib/aidp/harness/state_manager.rb +3 -0
- data/lib/aidp/harness/status_display.rb +28 -20
- data/lib/aidp/harness/thinking_depth_manager.rb +32 -32
- data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
- data/lib/aidp/harness/ui/error_handler.rb +3 -0
- data/lib/aidp/harness/ui/job_monitor.rb +4 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +2 -0
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
- data/lib/aidp/harness/ui.rb +11 -0
- data/lib/aidp/harness/user_interface.rb +3 -0
- data/lib/aidp/loader.rb +2 -2
- data/lib/aidp/logger.rb +3 -0
- data/lib/aidp/message_display.rb +31 -0
- data/lib/aidp/pr_worktree_manager.rb +18 -6
- data/lib/aidp/provider_manager.rb +3 -0
- data/lib/aidp/providers/base.rb +2 -0
- data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
- data/lib/aidp/security/secrets_proxy.rb +328 -0
- data/lib/aidp/security/secrets_registry.rb +227 -0
- data/lib/aidp/security/trifecta_state.rb +220 -0
- data/lib/aidp/security/watch_mode_handler.rb +306 -0
- data/lib/aidp/security/work_loop_adapter.rb +277 -0
- data/lib/aidp/security.rb +56 -0
- data/lib/aidp/setup/wizard.rb +4 -2
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/auto_merger.rb +274 -0
- data/lib/aidp/watch/auto_pr_processor.rb +125 -7
- data/lib/aidp/watch/build_processor.rb +16 -1
- data/lib/aidp/watch/change_request_processor.rb +680 -286
- data/lib/aidp/watch/ci_fix_processor.rb +262 -4
- data/lib/aidp/watch/feedback_collector.rb +191 -0
- data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
- data/lib/aidp/watch/implementation_verifier.rb +142 -1
- data/lib/aidp/watch/plan_generator.rb +70 -13
- data/lib/aidp/watch/plan_processor.rb +12 -5
- data/lib/aidp/watch/projects_processor.rb +286 -0
- data/lib/aidp/watch/repository_client.rb +861 -53
- data/lib/aidp/watch/review_processor.rb +33 -6
- data/lib/aidp/watch/runner.rb +51 -11
- data/lib/aidp/watch/state_store.rb +233 -0
- data/lib/aidp/watch/sub_issue_creator.rb +221 -0
- data/lib/aidp/workflows/guided_agent.rb +4 -0
- data/lib/aidp/workstream_executor.rb +3 -0
- data/lib/aidp/worktree.rb +61 -11
- data/lib/aidp/worktree_branch_manager.rb +347 -101
- data/templates/implementation/iterative_implementation.md +46 -3
- metadata +21 -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
|
|
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
|
-
|
|
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
|
-
#{
|
|
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"],
|
|
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],
|
|
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,
|
|
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,
|
|
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
|