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 +4 -4
- data/lib/aidp/config.rb +15 -1
- data/lib/aidp/setup/wizard.rb +14 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/gantt_synchronizer.rb +273 -0
- data/lib/aidp/watch/plan_generator.rb +2 -1
- data/lib/aidp/watch/plan_processor.rb +494 -18
- data/lib/aidp/watch/prd_parser.rb +395 -0
- data/lib/aidp/watch/projects_processor.rb +88 -11
- data/lib/aidp/watch/repository_client.rb +212 -1
- data/lib/aidp/watch/runner.rb +117 -9
- data/lib/aidp/watch/state_store.rb +41 -1
- data/lib/aidp/watch/sub_issue_creator.rb +133 -19
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a86b02df6fbe151abffa67c335c73fc3c724de523065f1f03b083bca305c1c6c
|
|
4
|
+
data.tar.gz: 15cf32cc8d3007f91e7a94a777e1b32d1a10002a63f91f10721bcf7ffe5c3d04
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
data/lib/aidp/setup/wizard.rb
CHANGED
|
@@ -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
|
@@ -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
|
|