aidp 0.43.0 โ†’ 0.45.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.
@@ -9,12 +9,16 @@ module Aidp
9
9
  class SubIssueCreator
10
10
  include Aidp::MessageDisplay
11
11
 
12
+ UnresolvedDependenciesError = Class.new(StandardError)
13
+
12
14
  attr_reader :repository_client, :state_store, :project_id
13
15
 
14
- def initialize(repository_client:, state_store:, project_id: nil)
16
+ def initialize(repository_client:, state_store:, project_id: nil, build_label: "aidp-build", blocked_label: "aidp-blocked")
15
17
  @repository_client = repository_client
16
18
  @state_store = state_store
17
19
  @project_id = project_id
20
+ @build_label = build_label
21
+ @blocked_label = blocked_label
18
22
  end
19
23
 
20
24
  # Creates sub-issues from hierarchical plan data
@@ -28,22 +32,29 @@ module Aidp
28
32
  display_message("๐Ÿ”จ Creating #{sub_issues_data.size} sub-issues for ##{parent_number}", type: :info)
29
33
 
30
34
  created_issues = []
35
+ creation_failed = false
31
36
 
32
37
  sub_issues_data.each_with_index do |sub_data, index|
33
38
  issue = create_single_sub_issue(parent_issue, sub_data, index + 1)
34
39
  created_issues << issue
35
40
  display_message(" โœ“ Created sub-issue ##{issue[:number]}: #{sub_data[:title]}", type: :success)
36
41
  rescue => e
42
+ creation_failed = true
37
43
  Aidp.log_error("sub_issue_creator", "Failed to create sub-issue", parent: parent_number, index: index, error: e.message)
38
44
  display_message(" โœ— Failed to create sub-issue #{index + 1}: #{e.message}", type: :error)
45
+ break
39
46
  end
40
47
 
48
+ return handle_partial_creation_failure(parent_number, created_issues) if creation_failed
49
+
50
+ record_dependencies(created_issues)
51
+
41
52
  # Link all created issues to project if project_id is configured
42
53
  if @project_id && created_issues.any?
43
54
  link_issues_to_project(parent_number, created_issues.map { |i| i[:number] })
44
55
  end
45
56
 
46
- # Update state store with parent-child relationships
57
+ # Persist the hierarchy only after creation and dependency resolution succeed.
47
58
  @state_store.record_sub_issues(parent_number, created_issues.map { |i| i[:number] })
48
59
 
49
60
  # Post summary comment on parent issue
@@ -51,6 +62,23 @@ module Aidp
51
62
 
52
63
  Aidp.log_debug("sub_issue_creator", "create_sub_issues_complete", parent: parent_number, created: created_issues.size)
53
64
  created_issues
65
+ rescue UnresolvedDependenciesError
66
+ cleanup_created_issues(parent_number, created_issues)
67
+ raise
68
+ end
69
+
70
+ def planned_sub_issue_attributes(parent_issue, sub_data, sequence_number)
71
+ dependencies = sub_issue_dependencies(sub_data)
72
+ title = sub_data[:title]
73
+ title = "#{parent_issue[:title]} - Part #{sequence_number}" if title.to_s.strip.empty?
74
+
75
+ {
76
+ title: title,
77
+ body: build_sub_issue_body(parent_issue, sub_data, sequence_number),
78
+ labels: sub_issue_labels(sub_data, dependencies),
79
+ assignees: Array(sub_data[:assignees]),
80
+ dependencies: dependencies
81
+ }
54
82
  end
55
83
 
56
84
  private
@@ -59,23 +87,14 @@ module Aidp
59
87
  parent_number = parent_issue[:number]
60
88
  Aidp.log_debug("sub_issue_creator", "create_single_sub_issue", parent: parent_number, sequence: sequence_number)
61
89
 
62
- # Build issue title
63
- title = sub_data[:title]
64
- title = "#{parent_issue[:title]} - Part #{sequence_number}" if title.to_s.strip.empty?
65
-
66
- # Build issue body with parent reference
67
- body = build_sub_issue_body(parent_issue, sub_data, sequence_number)
68
-
69
- # Determine labels
70
- labels = ["aidp-auto"]
71
- labels.concat(Array(sub_data[:labels])) if sub_data[:labels]
90
+ attributes = planned_sub_issue_attributes(parent_issue, sub_data, sequence_number)
72
91
 
73
92
  # Create the issue
74
93
  result = @repository_client.create_issue(
75
- title: title,
76
- body: body,
77
- labels: labels,
78
- assignees: Array(sub_data[:assignees])
94
+ title: attributes[:title],
95
+ body: attributes[:body],
96
+ labels: attributes[:labels],
97
+ assignees: attributes[:assignees]
79
98
  )
80
99
 
81
100
  Aidp.log_debug("sub_issue_creator", "issue_created", parent: parent_number, number: result[:number], url: result[:url])
@@ -83,10 +102,10 @@ module Aidp
83
102
  {
84
103
  number: result[:number],
85
104
  url: result[:url],
86
- title: title,
105
+ title: attributes[:title],
87
106
  skills: sub_data[:skills],
88
107
  personas: sub_data[:personas],
89
- dependencies: sub_data[:dependencies]
108
+ dependencies: attributes[:dependencies]
90
109
  }
91
110
  end
92
111
 
@@ -201,7 +220,8 @@ module Aidp
201
220
  end
202
221
 
203
222
  parts << ""
204
- parts << "Each sub-issue has been labeled with `aidp-auto` and will be automatically picked up for implementation."
223
+ parts << "Dependency-ready sub-issues have been labeled with `#{@build_label}`."
224
+ parts << "Sub-issues waiting on prerequisites have been labeled with `#{@blocked_label}` until their dependencies are complete."
205
225
  parts << ""
206
226
  parts << "---"
207
227
  parts << "_This breakdown was automatically generated by AIDP._"
@@ -216,6 +236,100 @@ module Aidp
216
236
  display_message("โš ๏ธ Failed to post summary comment: #{e.message}", type: :warn)
217
237
  end
218
238
  end
239
+
240
+ def record_dependencies(created_issues)
241
+ title_map = build_title_map(created_issues)
242
+
243
+ created_issues.each do |issue|
244
+ dependency_numbers, unresolved_dependencies = resolve_dependency_numbers(issue[:dependencies], title_map)
245
+ raise_unresolved_dependencies!(issue, unresolved_dependencies) if unresolved_dependencies.any?
246
+
247
+ @state_store.record_issue_dependencies(issue[:number], dependency_numbers)
248
+ issue[:dependencies] = dependency_numbers
249
+ end
250
+ end
251
+
252
+ def resolve_dependency_numbers(dependencies, title_map)
253
+ Array(dependencies).each_with_object([[], []]) do |dependency, (resolved, unresolved)|
254
+ dependency_number = resolve_dependency_number(dependency, title_map)
255
+ dependency_number ? resolved << dependency_number : unresolved << dependency
256
+ end
257
+ end
258
+
259
+ def raise_unresolved_dependencies!(issue, unresolved_dependencies)
260
+ dependency_list = unresolved_dependencies.join(", ")
261
+ raise UnresolvedDependenciesError,
262
+ "Unable to resolve dependencies for sub-issue ##{issue[:number]} (#{issue[:title]}): #{dependency_list}"
263
+ end
264
+
265
+ def build_title_map(created_issues)
266
+ duplicate_titles = duplicate_normalized_titles(created_issues)
267
+ if duplicate_titles.empty?
268
+ return created_issues.each_with_object({}) do |issue, memo|
269
+ memo[normalize_title(issue[:title])] = issue[:number]
270
+ end
271
+ end
272
+
273
+ raise UnresolvedDependenciesError,
274
+ "Duplicate sub-issue titles after normalization are not allowed: #{duplicate_titles.join(", ")}"
275
+ end
276
+
277
+ def duplicate_normalized_titles(created_issues)
278
+ grouped_titles = created_issues.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |issue, memo|
279
+ normalized_title = normalize_title(issue[:title])
280
+ next if normalized_title.empty?
281
+
282
+ memo[normalized_title] << issue[:title].to_s.strip
283
+ end
284
+
285
+ grouped_titles.filter_map do |_normalized_title, titles|
286
+ next unless titles.size > 1
287
+
288
+ titles.uniq.join(" / ")
289
+ end
290
+ end
291
+
292
+ def normalize_title(title)
293
+ title.to_s.strip.downcase
294
+ end
295
+
296
+ def resolve_dependency_number(dependency, title_map)
297
+ text = dependency.to_s.strip
298
+ match = text.match(/\A#(\d+)\z/)
299
+ return match[1].to_i if match
300
+
301
+ title_map[normalize_title(text)]
302
+ end
303
+
304
+ def sub_issue_dependencies(sub_data)
305
+ Array(sub_data[:dependencies]).map(&:to_s).reject(&:empty?)
306
+ end
307
+
308
+ def sub_issue_labels(sub_data, dependencies)
309
+ [dependencies.any? ? @blocked_label : @build_label].concat(Array(sub_data[:labels]))
310
+ end
311
+
312
+ def handle_partial_creation_failure(parent_number, created_issues)
313
+ cleanup_created_issues(parent_number, created_issues)
314
+ []
315
+ end
316
+
317
+ def cleanup_created_issues(parent_number, created_issues)
318
+ return if created_issues.empty?
319
+
320
+ Aidp.log_warn("sub_issue_creator", "cleanup_created_sub_issues",
321
+ parent: parent_number,
322
+ sub_issue_numbers: created_issues.map { |issue| issue[:number] })
323
+ display_message("๐Ÿงน Cleaning up #{created_issues.size} partially created sub-issues for ##{parent_number}", type: :warn)
324
+
325
+ created_issues.each do |issue|
326
+ @repository_client.close_issue(issue[:number])
327
+ display_message(" โœ“ Closed sub-issue ##{issue[:number]}", type: :success)
328
+ rescue => e
329
+ Aidp.log_error("sub_issue_creator", "Failed to clean up sub-issue", issue: issue[:number], error: e.message)
330
+ display_message(" โœ— Failed to close sub-issue ##{issue[:number]}: #{e.message}", type: :warn)
331
+ end
332
+ end
219
333
  end
220
334
  end
221
335
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aidp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.43.0
4
+ version: 0.45.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan
@@ -609,11 +609,13 @@ files:
609
609
  - lib/aidp/watch/ci_fix_processor.rb
610
610
  - lib/aidp/watch/ci_log_extractor.rb
611
611
  - lib/aidp/watch/feedback_collector.rb
612
+ - lib/aidp/watch/gantt_synchronizer.rb
612
613
  - lib/aidp/watch/github_state_extractor.rb
613
614
  - lib/aidp/watch/hierarchical_pr_strategy.rb
614
615
  - lib/aidp/watch/implementation_verifier.rb
615
616
  - lib/aidp/watch/plan_generator.rb
616
617
  - lib/aidp/watch/plan_processor.rb
618
+ - lib/aidp/watch/prd_parser.rb
617
619
  - lib/aidp/watch/projects_processor.rb
618
620
  - lib/aidp/watch/rebase_processor.rb
619
621
  - lib/aidp/watch/repository_client.rb