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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 013dc044889cc320404776d0691d2091f3cddda0d9ba854dc0afb92312396336
4
- data.tar.gz: bb1c57bf8e7d87dfa7d7b169d69646ac76c0cb14fe2f72c70ef02b8543b43494
3
+ metadata.gz: a86b02df6fbe151abffa67c335c73fc3c724de523065f1f03b083bca305c1c6c
4
+ data.tar.gz: 15cf32cc8d3007f91e7a94a777e1b32d1a10002a63f91f10721bcf7ffe5c3d04
5
5
  SHA512:
6
- metadata.gz: c7ea760cd1d62424ada60f64acc356cdf4a82db6e2166c203d9d41c489fed936a704afaee17e9a20d588fbf0ead8ebd816a991a3fa01d87644939c872b104b12
7
- data.tar.gz: 9c3b34d7474a2f9e899bf50d9b61d1edcb67a1b4a7b41d78abf810517db8cb2082fe889ec7b1fda5266276b6f1406e577644f2ddca16142bcf66e66e79b1e338
6
+ metadata.gz: dc49aca1c586e8603be81e229ad3ce42663a5d9592995cc593ed7929c48339542cb67d8b9c3056b429d8409b016c2ddcc4619938f9905666d23a3f0ced1cf006
7
+ data.tar.gz: 1dac0b372011909722cc0d139bee899b4b968d3041fba11114f7314a9ce5b5a7b8037799b3484e1c03f8587312aae507bad44469c29bbb9b102c6035ad45dea0
data/lib/aidp/config.rb CHANGED
@@ -197,7 +197,9 @@ module Aidp
197
197
  polling_interval: 30,
198
198
  labels: {
199
199
  plan_trigger: "aidp-plan",
200
+ project_trigger: "aidp-project",
200
201
  build_trigger: "aidp-build",
202
+ blocked_trigger: "aidp-blocked",
201
203
  review_trigger: "aidp-review",
202
204
  fix_ci_trigger: "aidp-fix-ci",
203
205
  change_request_trigger: "aidp-request-changes",
@@ -213,10 +215,17 @@ module Aidp
213
215
  priority: "Priority",
214
216
  skills: "Skills",
215
217
  personas: "Personas",
216
- blocking: "Blocking"
218
+ blocking: "Blocking",
219
+ start_date: "Start Date",
220
+ target_date: "Target Date",
221
+ dependencies: "Dependencies",
222
+ critical_path: "Critical Path"
217
223
  },
218
224
  auto_create_fields: true,
219
225
  sync_interval: 60,
226
+ prd_path: nil,
227
+ auto_sync_gantt: false,
228
+ gantt_format: "auto",
220
229
  default_status_values: ["Backlog", "Todo", "In Progress", "In Review", "Done"],
221
230
  default_priority_values: ["Low", "Medium", "High", "Critical"]
222
231
  },
@@ -510,6 +519,11 @@ module Aidp
510
519
  merged[:security] = deep_merge_hash(merged[:security], symbolize_keys(security_section))
511
520
  end
512
521
 
522
+ if config[:watch] || config["watch"]
523
+ watch_section = config[:watch] || config["watch"]
524
+ merged[:watch] = deep_merge_hash(merged[:watch], symbolize_keys(watch_section))
525
+ end
526
+
513
527
  merged
514
528
  end
515
529
 
@@ -1649,6 +1649,11 @@ module Aidp
1649
1649
  existing[:plan_trigger] || "aidp-plan"
1650
1650
  )
1651
1651
 
1652
+ project_trigger = ask_with_default(
1653
+ "Label to trigger project planning",
1654
+ existing[:project_trigger] || "aidp-project"
1655
+ )
1656
+
1652
1657
  needs_input = ask_with_default(
1653
1658
  "Label for plans needing user input",
1654
1659
  existing[:needs_input] || "aidp-needs-input"
@@ -1664,6 +1669,11 @@ module Aidp
1664
1669
  existing[:build_trigger] || "aidp-build"
1665
1670
  )
1666
1671
 
1672
+ blocked_trigger = ask_with_default(
1673
+ "Label for blocked project sub-issues",
1674
+ existing[:blocked_trigger] || "aidp-blocked"
1675
+ )
1676
+
1667
1677
  review_trigger = ask_with_default(
1668
1678
  "Label to trigger code review",
1669
1679
  existing[:review_trigger] || "aidp-review"
@@ -1686,9 +1696,11 @@ module Aidp
1686
1696
 
1687
1697
  set(%i[watch labels], {
1688
1698
  plan_trigger: plan_trigger,
1699
+ project_trigger: project_trigger,
1689
1700
  needs_input: needs_input,
1690
1701
  ready_to_build: ready_to_build,
1691
1702
  build_trigger: build_trigger,
1703
+ blocked_trigger: blocked_trigger,
1692
1704
  review_trigger: review_trigger,
1693
1705
  ci_fix_trigger: ci_fix_trigger,
1694
1706
  auto_trigger: auto_trigger,
@@ -1856,9 +1868,11 @@ module Aidp
1856
1868
  def collect_required_labels(labels_config)
1857
1869
  default_colors = {
1858
1870
  plan_trigger: "0E8A16", # Green
1871
+ project_trigger: "0052CC", # Deep blue
1859
1872
  needs_input: "D93F0B", # Red
1860
1873
  ready_to_build: "0075CA", # Blue
1861
1874
  build_trigger: "5319E7", # Purple
1875
+ blocked_trigger: "B60205", # Dark red
1862
1876
  review_trigger: "FBCA04", # Yellow
1863
1877
  ci_fix_trigger: "D93F0B", # Red
1864
1878
  auto_trigger: "0C8BD6", # Blue (distinct from build)
data/lib/aidp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.43.0"
4
+ VERSION = "0.45.0"
5
5
  end
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Aidp
6
+ module Watch
7
+ # Synchronizes parsed Gantt task data with GitHub Projects and Mermaid docs.
8
+ class GanttSynchronizer
9
+ CRITICAL_PATH_VALUES = %w[Yes No].freeze
10
+
11
+ def initialize(repository_client:, state_store:, project_id:, field_mappings:, auto_create_fields: true, parser_class: PrdParser)
12
+ @repository_client = repository_client
13
+ @state_store = state_store
14
+ @project_id = project_id
15
+ @field_mappings = field_mappings
16
+ @auto_create_fields = auto_create_fields
17
+ @parser_class = parser_class
18
+ @project_fields_cache = nil
19
+ end
20
+
21
+ def sync_from_prd(prd_path:, format: nil, issue_numbers_by_title: {})
22
+ parsed = @parser_class.new(file_path: prd_path, format: format).parse
23
+ tasks = resolve_issue_numbers(parsed[:tasks], issue_numbers_by_title)
24
+ critical_path = calculate_critical_path(tasks)
25
+ tasks_by_id = tasks.each_with_object({}) { |task, memo| memo[task[:id]] = task }
26
+
27
+ synced = 0
28
+ skipped = 0
29
+
30
+ tasks.each do |task|
31
+ if task[:issue_number].nil?
32
+ skipped += 1
33
+ next
34
+ end
35
+
36
+ item_id = ensure_project_item(task[:issue_number])
37
+ update_date_fields(item_id, task)
38
+ update_dependencies_field(item_id, task, tasks_by_id)
39
+ update_critical_path_field(item_id, critical_path.include?(task[:id]))
40
+
41
+ @state_store.record_project_sync(task[:issue_number], {
42
+ gantt_task_id: task[:id],
43
+ gantt_path: prd_path,
44
+ gantt_format: parsed[:format].to_s,
45
+ critical_path: critical_path.include?(task[:id])
46
+ })
47
+ synced += 1
48
+ end
49
+
50
+ {synced: synced, skipped: skipped, critical_path: critical_path}
51
+ end
52
+
53
+ def sync_issue_status_to_gantt(prd_path:, issue_number:, status:, format: nil)
54
+ parser = @parser_class.new(file_path: prd_path, format: format)
55
+ parsed = parser.parse
56
+ return false unless parsed[:format] == :mermaid
57
+
58
+ content = File.read(prd_path)
59
+ lines = content.lines
60
+ line_index = mermaid_task_line_index(content, parsed[:tasks], issue_number)
61
+ return false unless line_index
62
+
63
+ lines[line_index] = rewrite_mermaid_status(lines[line_index], status)
64
+ File.write(prd_path, lines.join)
65
+ true
66
+ end
67
+
68
+ private
69
+
70
+ def resolve_issue_numbers(tasks, issue_numbers_by_title)
71
+ tasks.map do |task|
72
+ next task if task[:issue_number]
73
+
74
+ resolved_issue_number = issue_numbers_by_title[normalize_title(task[:name])]
75
+ resolved_issue_number ? task.merge(issue_number: resolved_issue_number) : task
76
+ end
77
+ end
78
+
79
+ def calculate_critical_path(tasks)
80
+ explicit = tasks.select { |task| task[:critical] }.map { |task| task[:id] }
81
+ return explicit if explicit.any?
82
+
83
+ durations = tasks.each_with_object({}) { |task, memo| memo[task[:id]] = task[:duration_days].to_i }
84
+ predecessors = tasks.each_with_object({}) { |task, memo| memo[task[:id]] = task[:dependency_ids] }
85
+ longest = {}
86
+ previous = {}
87
+
88
+ ordered_ids(tasks, predecessors).each do |task_id|
89
+ best_predecessor = predecessors[task_id].max_by { |candidate| longest[candidate].to_i }
90
+ longest[task_id] = durations[task_id] + longest[best_predecessor].to_i
91
+ previous[task_id] = best_predecessor
92
+ end
93
+
94
+ endpoint = longest.max_by { |_task_id, distance| distance }&.first
95
+ unwind_path(endpoint, previous)
96
+ end
97
+
98
+ def ordered_ids(tasks, predecessors)
99
+ task_ids = tasks.map { |task| task[:id] }
100
+ ordered = []
101
+ remaining = task_ids.dup
102
+
103
+ until remaining.empty?
104
+ progressed = false
105
+ remaining.dup.each do |task_id|
106
+ unmet = predecessors[task_id].reject { |dependency| ordered.include?(dependency) }
107
+ next if unmet.any?
108
+
109
+ ordered << task_id
110
+ remaining.delete(task_id)
111
+ progressed = true
112
+ end
113
+
114
+ break unless progressed
115
+ end
116
+
117
+ ordered + remaining
118
+ end
119
+
120
+ def unwind_path(endpoint, previous)
121
+ path = []
122
+ current = endpoint
123
+
124
+ while current
125
+ path.unshift(current)
126
+ current = previous[current]
127
+ end
128
+
129
+ path
130
+ end
131
+
132
+ def ensure_project_item(issue_number)
133
+ item_id = @state_store.project_item_id(issue_number)
134
+ return item_id if item_id
135
+
136
+ item_id = @repository_client.link_issue_to_project(@project_id, issue_number)
137
+ @state_store.record_project_item_id(issue_number, item_id)
138
+ item_id
139
+ end
140
+
141
+ def update_date_fields(item_id, task)
142
+ update_date_field(item_id, @field_mappings[:start_date], task[:start_date])
143
+ update_date_field(item_id, @field_mappings[:target_date], task[:end_date] || task[:start_date])
144
+ end
145
+
146
+ def update_date_field(item_id, field_name, date)
147
+ return unless field_name && date
148
+
149
+ field = find_or_create_field(field_name, "DATE")
150
+ return unless field
151
+
152
+ @repository_client.update_project_item_field(
153
+ item_id,
154
+ field[:id],
155
+ {project_id: @project_id, date: date.iso8601}
156
+ )
157
+ end
158
+
159
+ def update_dependencies_field(item_id, task, tasks_by_id)
160
+ field_name = @field_mappings[:dependencies]
161
+ return unless field_name
162
+
163
+ dependency_issue_numbers = task[:dependency_ids].filter_map do |dependency_id|
164
+ tasks_by_id[dependency_id]&.dig(:issue_number)
165
+ end
166
+ field = find_or_create_field(field_name, "TEXT")
167
+ return unless field
168
+
169
+ text = dependency_issue_numbers.map { |issue_number| "##{issue_number}" }.join(", ")
170
+ @repository_client.update_project_item_field(
171
+ item_id,
172
+ field[:id],
173
+ {project_id: @project_id, text: text}
174
+ )
175
+ end
176
+
177
+ def update_critical_path_field(item_id, critical)
178
+ field_name = @field_mappings[:critical_path]
179
+ return unless field_name
180
+
181
+ field = find_or_create_field(field_name, "SINGLE_SELECT", CRITICAL_PATH_VALUES)
182
+ return unless field
183
+
184
+ option_id = field[:options]&.find { |option| option[:name].casecmp?(critical ? "Yes" : "No") }&.dig(:id)
185
+ return unless option_id
186
+
187
+ @repository_client.update_project_item_field(
188
+ item_id,
189
+ field[:id],
190
+ {project_id: @project_id, option_id: option_id}
191
+ )
192
+ end
193
+
194
+ def rewrite_mermaid_status(line, status)
195
+ match = line.match(/\A(?<name_part>.*?)(?<separator>\s*:\s*)(?<definition>.*?)(?<newline>\r?\n?)\z/)
196
+ return line unless match
197
+
198
+ mapped_status = mermaid_status_for(status)
199
+
200
+ tokens = match[:definition].split(",").map(&:strip).reject(&:empty?)
201
+ tokens.reject! { |token| %w[done active].include?(token) }
202
+
203
+ tokens.unshift(mapped_status) if mapped_status
204
+
205
+ "#{match[:name_part]}#{match[:separator]}#{tokens.join(", ")}#{match[:newline]}"
206
+ end
207
+
208
+ def mermaid_status_for(status)
209
+ case status.to_s.downcase
210
+ when "done", "closed", "completed" then "done"
211
+ when "in progress", "in_progress", "active" then "active"
212
+ end
213
+ end
214
+
215
+ def mermaid_task_line_index(content, tasks, issue_number)
216
+ line_number = find_mermaid_task(tasks, issue_number)&.dig(:line_number)
217
+ return unless line_number
218
+
219
+ line_number + mermaid_chart_line_offset(content) - 1
220
+ end
221
+
222
+ def mermaid_chart_line_offset(content)
223
+ lines = content.lines
224
+ mermaid_start = lines.each_index.find do |index|
225
+ mermaid_gantt_block?(lines, index)
226
+ end
227
+ mermaid_start ? mermaid_start + 1 : 0
228
+ end
229
+
230
+ def find_mermaid_task(tasks, issue_number)
231
+ sync_data = @state_store.project_sync_data(issue_number)
232
+ gantt_task_id = sync_data["gantt_task_id"]
233
+
234
+ tasks.find { |task| task[:id] == gantt_task_id } ||
235
+ tasks.find { |task| task[:issue_number] == issue_number }
236
+ end
237
+
238
+ def mermaid_gantt_block?(lines, start_index)
239
+ return false unless lines[start_index].match?(/\A```mermaid\b/)
240
+
241
+ block_lines = lines[(start_index + 1)..]
242
+ return false unless block_lines
243
+
244
+ block_lines.take_while { |line| !line.match?(/\A\s*```\s*$/) }.any? do |line|
245
+ line.strip == "gantt"
246
+ end
247
+ end
248
+
249
+ def normalize_title(title)
250
+ title.to_s.gsub(/\s*\(#\d+\)\s*/, " ").strip.downcase
251
+ end
252
+
253
+ def project_fields
254
+ @project_fields_cache ||= @repository_client.fetch_project_fields(@project_id)
255
+ end
256
+
257
+ def invalidate_fields_cache
258
+ @project_fields_cache = nil
259
+ end
260
+
261
+ def find_or_create_field(name, field_type, options = nil)
262
+ field = project_fields.find { |entry| entry[:name].casecmp?(name) }
263
+ return field if field
264
+ return nil unless @auto_create_fields
265
+
266
+ formatted_options = options&.map { |option| {name: option} } if field_type == "SINGLE_SELECT"
267
+ field = @repository_client.create_project_field(@project_id, name, field_type, options: formatted_options)
268
+ invalidate_fields_cache
269
+ field
270
+ end
271
+ end
272
+ end
273
+ end
@@ -27,7 +27,7 @@ module Aidp
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.
30
+ You are AIDP's project planning specialist for large projects. Read the GitHub issue and existing comments.
31
31
  This is a LARGE project that should be broken down into independent sub-issues.
32
32
 
33
33
  Analyze the requirements and break them into logical sub-tasks that can be worked on independently.
@@ -59,6 +59,7 @@ module Aidp
59
59
  - Sub-issues should be sized for 1-3 days of work
60
60
  - Include 3-8 sub-issues for a large project
61
61
  - Be specific about skills needed (avoid generic terms)
62
+ - Prefer leaf-node tasks first so dependency order is explicit and mechanically executable
62
63
  - Only create sub-issues if the project truly warrants it (complexity, multiple components, etc.)
63
64
  PROMPT
64
65