aidp 0.43.0 → 0.44.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 +2 -0
- data/lib/aidp/setup/wizard.rb +14 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/plan_generator.rb +2 -1
- data/lib/aidp/watch/plan_processor.rb +472 -18
- data/lib/aidp/watch/repository_client.rb +194 -0
- 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 +1 -1
|
@@ -303,11 +303,35 @@ module Aidp
|
|
|
303
303
|
create_project_field_via_gh(project_id, name, field_type, options: options)
|
|
304
304
|
end
|
|
305
305
|
|
|
306
|
+
def create_project(title:, repository_id: nil)
|
|
307
|
+
raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
|
|
308
|
+
create_project_via_gh(title: title, repository_id: repository_id)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def find_active_project
|
|
312
|
+
raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
|
|
313
|
+
find_active_project_via_gh
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def repository_node_data
|
|
317
|
+
raise "GitHub CLI not available - Projects API requires gh CLI" unless gh_available?
|
|
318
|
+
repository_node_data_via_gh
|
|
319
|
+
end
|
|
320
|
+
|
|
306
321
|
def create_issue(title:, body:, labels: [], assignees: [])
|
|
307
322
|
raise "GitHub CLI not available - cannot create issue" unless gh_available?
|
|
308
323
|
create_issue_via_gh(title: title, body: body, labels: labels, assignees: assignees)
|
|
309
324
|
end
|
|
310
325
|
|
|
326
|
+
def close_issue(number)
|
|
327
|
+
raise "GitHub CLI not available - cannot close issue" unless gh_available?
|
|
328
|
+
close_issue_via_gh(number)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def update_issue(number, title:, body:, labels:, assignees:)
|
|
332
|
+
gh_available? ? update_issue_via_gh(number, title: title, body: body, labels: labels, assignees: assignees) : update_issue_via_api(number, title: title, body: body, labels: labels, assignees: assignees)
|
|
333
|
+
end
|
|
334
|
+
|
|
311
335
|
def merge_pull_request(number, merge_method: "squash")
|
|
312
336
|
raise "GitHub CLI not available - cannot merge PR" unless gh_available?
|
|
313
337
|
merge_pull_request_via_gh(number, merge_method: merge_method)
|
|
@@ -430,6 +454,47 @@ module Aidp
|
|
|
430
454
|
end
|
|
431
455
|
end
|
|
432
456
|
|
|
457
|
+
def update_issue_via_gh(number, title:, body:, labels:, assignees:)
|
|
458
|
+
with_gh_retry("update_issue") do
|
|
459
|
+
existing_issue = fetch_issue_via_gh(number)
|
|
460
|
+
cmd = ["gh", "issue", "edit", number.to_s, "--repo", full_repo, "--title", title, "--body", body]
|
|
461
|
+
|
|
462
|
+
label_changes(existing_issue[:labels], labels).each do |flag, values|
|
|
463
|
+
values.each { |value| cmd.concat([flag, value]) }
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
assignee_changes(existing_issue[:assignees], assignees).each do |flag, values|
|
|
467
|
+
values.each { |value| cmd.concat([flag, value]) }
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
_stdout, stderr, status = Open3.capture3(*cmd)
|
|
471
|
+
raise "Failed to update issue via gh: #{stderr.strip}" unless status.success?
|
|
472
|
+
|
|
473
|
+
true
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def update_issue_via_api(number, title:, body:, labels:, assignees:)
|
|
478
|
+
uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}")
|
|
479
|
+
request = Net::HTTP::Patch.new(uri)
|
|
480
|
+
request["Content-Type"] = "application/json"
|
|
481
|
+
request["Accept"] = "application/vnd.github.v3+json"
|
|
482
|
+
request.body = JSON.dump({
|
|
483
|
+
title: title,
|
|
484
|
+
body: body,
|
|
485
|
+
labels: labels,
|
|
486
|
+
assignees: assignees
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
490
|
+
http.request(request)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
raise "GitHub API issue update failed (#{response.code})" unless response.code.start_with?("2")
|
|
494
|
+
|
|
495
|
+
true
|
|
496
|
+
end
|
|
497
|
+
|
|
433
498
|
def post_comment_via_api(number, body)
|
|
434
499
|
uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/comments")
|
|
435
500
|
request = Net::HTTP::Post.new(uri)
|
|
@@ -1358,6 +1423,20 @@ module Aidp
|
|
|
1358
1423
|
end
|
|
1359
1424
|
end
|
|
1360
1425
|
|
|
1426
|
+
def label_changes(existing_labels, next_labels)
|
|
1427
|
+
{
|
|
1428
|
+
"--remove-label" => Array(existing_labels) - Array(next_labels),
|
|
1429
|
+
"--add-label" => Array(next_labels) - Array(existing_labels)
|
|
1430
|
+
}
|
|
1431
|
+
end
|
|
1432
|
+
|
|
1433
|
+
def assignee_changes(existing_assignees, next_assignees)
|
|
1434
|
+
{
|
|
1435
|
+
"--remove-assignee" => Array(existing_assignees) - Array(next_assignees),
|
|
1436
|
+
"--add-assignee" => Array(next_assignees) - Array(existing_assignees)
|
|
1437
|
+
}
|
|
1438
|
+
end
|
|
1439
|
+
|
|
1361
1440
|
def normalize_pr_comment(raw)
|
|
1362
1441
|
{
|
|
1363
1442
|
id: raw["id"],
|
|
@@ -1716,6 +1795,105 @@ module Aidp
|
|
|
1716
1795
|
raise
|
|
1717
1796
|
end
|
|
1718
1797
|
|
|
1798
|
+
def repository_node_data_via_gh
|
|
1799
|
+
Aidp.log_debug("repository_client", "repository_node_data", repo: full_repo)
|
|
1800
|
+
|
|
1801
|
+
query = <<~GRAPHQL
|
|
1802
|
+
query($owner: String!, $repo: String!) {
|
|
1803
|
+
repository(owner: $owner, name: $repo) {
|
|
1804
|
+
id
|
|
1805
|
+
owner {
|
|
1806
|
+
__typename
|
|
1807
|
+
login
|
|
1808
|
+
... on Organization {
|
|
1809
|
+
id
|
|
1810
|
+
}
|
|
1811
|
+
... on User {
|
|
1812
|
+
id
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
GRAPHQL
|
|
1818
|
+
|
|
1819
|
+
result = execute_graphql_query(query, owner: owner, repo: repo)
|
|
1820
|
+
repo_data = result.dig("data", "repository")
|
|
1821
|
+
raise "Repository not found: #{full_repo}" unless repo_data
|
|
1822
|
+
|
|
1823
|
+
{
|
|
1824
|
+
repository_id: repo_data["id"],
|
|
1825
|
+
owner_id: repo_data.dig("owner", "id"),
|
|
1826
|
+
owner_login: repo_data.dig("owner", "login"),
|
|
1827
|
+
owner_type: repo_data.dig("owner", "__typename")
|
|
1828
|
+
}
|
|
1829
|
+
rescue => e
|
|
1830
|
+
Aidp.log_error("repository_client", "repository_node_data_failed", repo: full_repo, error: e.message)
|
|
1831
|
+
raise
|
|
1832
|
+
end
|
|
1833
|
+
|
|
1834
|
+
def create_project_via_gh(title:, repository_id: nil)
|
|
1835
|
+
owner_data = repository_node_data_via_gh
|
|
1836
|
+
|
|
1837
|
+
mutation = <<~GRAPHQL
|
|
1838
|
+
mutation($ownerId: ID!, $title: String!, $repositoryId: ID) {
|
|
1839
|
+
createProjectV2(input: {
|
|
1840
|
+
ownerId: $ownerId
|
|
1841
|
+
title: $title
|
|
1842
|
+
repositoryId: $repositoryId
|
|
1843
|
+
}) {
|
|
1844
|
+
projectV2 {
|
|
1845
|
+
id
|
|
1846
|
+
title
|
|
1847
|
+
number
|
|
1848
|
+
url
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
GRAPHQL
|
|
1853
|
+
|
|
1854
|
+
result = execute_graphql_query(
|
|
1855
|
+
mutation,
|
|
1856
|
+
ownerId: owner_data[:owner_id],
|
|
1857
|
+
title: title,
|
|
1858
|
+
repositoryId: repository_id || owner_data[:repository_id]
|
|
1859
|
+
)
|
|
1860
|
+
project_data = result.dig("data", "createProjectV2", "projectV2")
|
|
1861
|
+
raise "Failed to create project: #{title}" unless project_data
|
|
1862
|
+
|
|
1863
|
+
normalize_project(project_data)
|
|
1864
|
+
rescue => e
|
|
1865
|
+
Aidp.log_error("repository_client", "create_project_failed", repo: full_repo, title: title, error: e.message)
|
|
1866
|
+
raise
|
|
1867
|
+
end
|
|
1868
|
+
|
|
1869
|
+
def find_active_project_via_gh
|
|
1870
|
+
Aidp.log_debug("repository_client", "find_active_project", repo: full_repo)
|
|
1871
|
+
|
|
1872
|
+
query = <<~GRAPHQL
|
|
1873
|
+
query($owner: String!, $repo: String!) {
|
|
1874
|
+
repository(owner: $owner, name: $repo) {
|
|
1875
|
+
projectsV2(first: 20, orderBy: {field: UPDATED_AT, direction: DESC}) {
|
|
1876
|
+
nodes {
|
|
1877
|
+
id
|
|
1878
|
+
title
|
|
1879
|
+
number
|
|
1880
|
+
url
|
|
1881
|
+
closed
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
GRAPHQL
|
|
1887
|
+
|
|
1888
|
+
result = execute_graphql_query(query, owner: owner, repo: repo)
|
|
1889
|
+
projects = Array(result.dig("data", "repository", "projectsV2", "nodes"))
|
|
1890
|
+
active_project = projects.find { |project| !project["closed"] }
|
|
1891
|
+
normalize_project(active_project) if active_project
|
|
1892
|
+
rescue => e
|
|
1893
|
+
Aidp.log_error("repository_client", "find_active_project_failed", repo: full_repo, error: e.message)
|
|
1894
|
+
raise
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1719
1897
|
def create_issue_via_gh(title:, body:, labels: [], assignees: [])
|
|
1720
1898
|
Aidp.log_debug("repository_client", "create_issue", title: title, label_count: labels.size, assignee_count: assignees.size)
|
|
1721
1899
|
|
|
@@ -1737,6 +1915,19 @@ module Aidp
|
|
|
1737
1915
|
raise
|
|
1738
1916
|
end
|
|
1739
1917
|
|
|
1918
|
+
def close_issue_via_gh(number)
|
|
1919
|
+
Aidp.log_debug("repository_client", "close_issue", issue_number: number)
|
|
1920
|
+
|
|
1921
|
+
cmd = ["gh", "issue", "close", number.to_s, "--repo", full_repo]
|
|
1922
|
+
_stdout, stderr, status = Open3.capture3(*cmd)
|
|
1923
|
+
raise "Failed to close issue via gh: #{stderr.strip}" unless status.success?
|
|
1924
|
+
|
|
1925
|
+
Aidp.log_debug("repository_client", "close_issue_complete", issue_number: number)
|
|
1926
|
+
rescue => e
|
|
1927
|
+
Aidp.log_error("repository_client", "Failed to close issue", issue_number: number, error: e.message)
|
|
1928
|
+
raise
|
|
1929
|
+
end
|
|
1930
|
+
|
|
1740
1931
|
def merge_pull_request_via_gh(number, merge_method: "squash")
|
|
1741
1932
|
Aidp.log_debug("repository_client", "merge_pull_request", number: number, merge_method: merge_method)
|
|
1742
1933
|
|
|
@@ -1785,11 +1976,14 @@ module Aidp
|
|
|
1785
1976
|
end
|
|
1786
1977
|
|
|
1787
1978
|
def normalize_project(raw)
|
|
1979
|
+
return nil unless raw
|
|
1980
|
+
|
|
1788
1981
|
{
|
|
1789
1982
|
id: raw["id"],
|
|
1790
1983
|
title: raw["title"],
|
|
1791
1984
|
number: raw["number"],
|
|
1792
1985
|
url: raw["url"],
|
|
1986
|
+
closed: raw["closed"],
|
|
1793
1987
|
fields: Array(raw.dig("fields", "nodes")).map { |field| normalize_project_field(field) }
|
|
1794
1988
|
}
|
|
1795
1989
|
end
|
data/lib/aidp/watch/runner.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "tty-prompt"
|
|
4
4
|
require_relative "feedback_collector"
|
|
5
5
|
require_relative "github_state_extractor"
|
|
6
|
+
require_relative "projects_processor"
|
|
6
7
|
require_relative "round_robin_scheduler"
|
|
7
8
|
require_relative "work_item"
|
|
8
9
|
require_relative "worktree_cleanup_job"
|
|
@@ -43,6 +44,7 @@ module Aidp
|
|
|
43
44
|
|
|
44
45
|
# Extract label configuration from safety_config (it's actually the full watch config)
|
|
45
46
|
label_config = safety_config[:labels] || safety_config["labels"] || {}
|
|
47
|
+
@project_config = safety_config[:projects] || safety_config["projects"] || {}
|
|
46
48
|
|
|
47
49
|
# Extract detection comment configuration (issue #280)
|
|
48
50
|
# Enabled by default, can be disabled in config
|
|
@@ -58,7 +60,8 @@ module Aidp
|
|
|
58
60
|
repository_client: @repository_client,
|
|
59
61
|
state_store: @state_store,
|
|
60
62
|
plan_generator: PlanGenerator.new(provider_name: provider_name, verbose: verbose),
|
|
61
|
-
label_config: label_config
|
|
63
|
+
label_config: label_config,
|
|
64
|
+
project_config: @project_config
|
|
62
65
|
)
|
|
63
66
|
@build_processor = BuildProcessor.new(
|
|
64
67
|
repository_client: @repository_client,
|
|
@@ -269,7 +272,7 @@ module Aidp
|
|
|
269
272
|
# Dispatch to processor
|
|
270
273
|
case item.processor_type
|
|
271
274
|
when :plan
|
|
272
|
-
@plan_processor.process(detailed)
|
|
275
|
+
@plan_processor.process(detailed, trigger_label: item.label)
|
|
273
276
|
when :build
|
|
274
277
|
# Check build completion at dispatch time (moved from collection for API efficiency)
|
|
275
278
|
if @state_extractor.build_completed?(detailed)
|
|
@@ -336,6 +339,7 @@ module Aidp
|
|
|
336
339
|
items = []
|
|
337
340
|
|
|
338
341
|
items.concat(collect_plan_work_items)
|
|
342
|
+
items.concat(collect_project_work_items)
|
|
339
343
|
items.concat(collect_build_work_items)
|
|
340
344
|
items.concat(collect_auto_issue_work_items)
|
|
341
345
|
items.concat(collect_review_work_items)
|
|
@@ -353,10 +357,12 @@ module Aidp
|
|
|
353
357
|
# @return [Array<WorkItem>]
|
|
354
358
|
def collect_plan_work_items
|
|
355
359
|
label = @plan_processor.plan_label
|
|
360
|
+
project_label = @plan_processor.project_label
|
|
356
361
|
issues = @repository_client.list_issues(labels: [label], state: "open")
|
|
357
362
|
|
|
358
363
|
issues.filter_map do |issue|
|
|
359
364
|
next unless issue_has_label?(issue, label)
|
|
365
|
+
next if issue_has_label?(issue, project_label)
|
|
360
366
|
|
|
361
367
|
WorkItem.new(
|
|
362
368
|
number: issue[:number],
|
|
@@ -371,30 +377,56 @@ module Aidp
|
|
|
371
377
|
[]
|
|
372
378
|
end
|
|
373
379
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
def collect_build_work_items
|
|
377
|
-
label = @build_processor.build_label
|
|
380
|
+
def collect_project_work_items
|
|
381
|
+
label = @plan_processor.project_label
|
|
378
382
|
issues = @repository_client.list_issues(labels: [label], state: "open")
|
|
379
383
|
|
|
380
384
|
issues.filter_map do |issue|
|
|
381
385
|
next unless issue_has_label?(issue, label)
|
|
382
386
|
|
|
383
|
-
# Note: build_completed check moved to dispatch phase to avoid
|
|
384
|
-
# API calls during collection (addresses rate limiting concerns)
|
|
385
387
|
WorkItem.new(
|
|
386
388
|
number: issue[:number],
|
|
387
389
|
item_type: :issue,
|
|
388
|
-
processor_type: :
|
|
390
|
+
processor_type: :plan,
|
|
389
391
|
label: label,
|
|
390
392
|
data: issue
|
|
391
393
|
)
|
|
392
394
|
end
|
|
395
|
+
rescue => e
|
|
396
|
+
Aidp.log_error("watch_runner", "collect_project_items_failed", error: e.message)
|
|
397
|
+
[]
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Collect work items for build triggers.
|
|
401
|
+
# @return [Array<WorkItem>]
|
|
402
|
+
def collect_build_work_items
|
|
403
|
+
unblock_dependency_ready_items
|
|
404
|
+
|
|
405
|
+
standard_build_work_items + ready_project_work_items
|
|
393
406
|
rescue => e
|
|
394
407
|
Aidp.log_error("watch_runner", "collect_build_items_failed", error: e.message)
|
|
395
408
|
[]
|
|
396
409
|
end
|
|
397
410
|
|
|
411
|
+
def unblock_dependency_ready_items
|
|
412
|
+
blocked_label = @plan_processor.blocked_label
|
|
413
|
+
blocked_issues = @repository_client.list_issues(labels: [blocked_label], state: "open")
|
|
414
|
+
|
|
415
|
+
blocked_issues.each do |issue|
|
|
416
|
+
next unless issue_has_label?(issue, blocked_label)
|
|
417
|
+
next unless dependencies_met_for_issue?(issue[:number])
|
|
418
|
+
|
|
419
|
+
@repository_client.replace_labels(
|
|
420
|
+
issue[:number],
|
|
421
|
+
old_labels: [blocked_label],
|
|
422
|
+
new_labels: [unblocked_label_for(issue[:number])]
|
|
423
|
+
)
|
|
424
|
+
sync_project_status_for_unblocked_issue(issue[:number])
|
|
425
|
+
end
|
|
426
|
+
rescue => e
|
|
427
|
+
Aidp.log_error("watch_runner", "unblock_dependency_ready_items_failed", error: e.message)
|
|
428
|
+
end
|
|
429
|
+
|
|
398
430
|
# Collect work items for auto issue triggers.
|
|
399
431
|
# @return [Array<WorkItem>]
|
|
400
432
|
def collect_auto_issue_work_items
|
|
@@ -529,6 +561,82 @@ module Aidp
|
|
|
529
561
|
end
|
|
530
562
|
end
|
|
531
563
|
|
|
564
|
+
def standard_build_work_items
|
|
565
|
+
label = @build_processor.build_label
|
|
566
|
+
project_label = @plan_processor.project_label
|
|
567
|
+
issues = @repository_client.list_issues(labels: [label], state: "open")
|
|
568
|
+
|
|
569
|
+
issues.filter_map do |issue|
|
|
570
|
+
next unless issue_has_label?(issue, label)
|
|
571
|
+
next if issue_has_label?(issue, project_label)
|
|
572
|
+
|
|
573
|
+
build_work_item_for(issue, label: label)
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def ready_project_work_items
|
|
578
|
+
label = @plan_processor.ready_label
|
|
579
|
+
issues = @repository_client.list_issues(labels: [label], state: "open")
|
|
580
|
+
|
|
581
|
+
issues.filter_map do |issue|
|
|
582
|
+
next unless issue_has_label?(issue, label)
|
|
583
|
+
next unless @state_store.sub_issues(issue[:number]).any?
|
|
584
|
+
|
|
585
|
+
build_work_item_for(issue, label: label)
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def build_work_item_for(issue, label:)
|
|
590
|
+
# Note: build_completed check moved to dispatch phase to avoid
|
|
591
|
+
# API calls during collection (addresses rate limiting concerns)
|
|
592
|
+
WorkItem.new(
|
|
593
|
+
number: issue[:number],
|
|
594
|
+
item_type: :issue,
|
|
595
|
+
processor_type: :build,
|
|
596
|
+
label: label,
|
|
597
|
+
data: issue
|
|
598
|
+
)
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def dependencies_met_for_issue?(issue_number)
|
|
602
|
+
blocking_status = @state_store.blocking_status(issue_number)
|
|
603
|
+
return false unless blocking_status[:blocked]
|
|
604
|
+
|
|
605
|
+
blocking_status[:blockers].all? do |blocker_number|
|
|
606
|
+
blocker = @repository_client.fetch_issue(blocker_number)
|
|
607
|
+
blocker[:state].to_s.casecmp("closed").zero?
|
|
608
|
+
end
|
|
609
|
+
rescue => e
|
|
610
|
+
Aidp.log_warn("watch_runner", "dependency_check_failed",
|
|
611
|
+
issue: issue_number, error: e.message)
|
|
612
|
+
false
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def unblocked_label_for(issue_number)
|
|
616
|
+
return @plan_processor.ready_label if @state_store.sub_issues(issue_number).any?
|
|
617
|
+
|
|
618
|
+
@build_processor.build_label
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def sync_project_status_for_unblocked_issue(issue_number)
|
|
622
|
+
project_id = @state_store.project_sync_data(issue_number)["project_id"]
|
|
623
|
+
return unless project_id
|
|
624
|
+
|
|
625
|
+
projects_processor = ProjectsProcessor.new(
|
|
626
|
+
repository_client: @repository_client,
|
|
627
|
+
state_store: @state_store,
|
|
628
|
+
project_id: project_id,
|
|
629
|
+
config: @project_config
|
|
630
|
+
)
|
|
631
|
+
return if projects_processor.sync_issue_to_project(issue_number, status: ProjectsProcessor::STATUS_VALUES[:todo])
|
|
632
|
+
|
|
633
|
+
Aidp.log_warn("watch_runner", "project_status_sync_failed",
|
|
634
|
+
issue: issue_number, project_id: project_id)
|
|
635
|
+
rescue => e
|
|
636
|
+
Aidp.log_warn("watch_runner", "project_status_sync_failed",
|
|
637
|
+
issue: issue_number, project_id: project_id, error: e.message)
|
|
638
|
+
end
|
|
639
|
+
|
|
532
640
|
# Restore from checkpoint if one exists (after auto-update)
|
|
533
641
|
def restore_from_checkpoint_if_exists
|
|
534
642
|
return unless @auto_update_coordinator.policy.enabled
|
|
@@ -373,6 +373,7 @@ module Aidp
|
|
|
373
373
|
|
|
374
374
|
def record_sub_issues(parent_number, sub_issue_numbers)
|
|
375
375
|
hierarchies[parent_number.to_s] ||= {}
|
|
376
|
+
clear_stale_sub_issue_tracking(parent_number, Array(sub_issue_numbers))
|
|
376
377
|
hierarchies[parent_number.to_s]["sub_issues"] = Array(sub_issue_numbers)
|
|
377
378
|
hierarchies[parent_number.to_s]["created_at"] = Time.now.utc.iso8601
|
|
378
379
|
|
|
@@ -385,8 +386,47 @@ module Aidp
|
|
|
385
386
|
save!
|
|
386
387
|
end
|
|
387
388
|
|
|
389
|
+
def clear_sub_issues(parent_number)
|
|
390
|
+
record_sub_issues(parent_number, [])
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def issue_dependencies(issue_number)
|
|
394
|
+
Array(hierarchies[issue_number.to_s]&.dig("dependencies"))
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def record_issue_dependencies(issue_number, dependency_numbers)
|
|
398
|
+
hierarchies[issue_number.to_s] ||= {}
|
|
399
|
+
hierarchies[issue_number.to_s]["dependencies"] = Array(dependency_numbers)
|
|
400
|
+
hierarchies[issue_number.to_s]["dependencies_updated_at"] = Time.now.utc.iso8601
|
|
401
|
+
save!
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
private
|
|
405
|
+
|
|
406
|
+
def clear_stale_sub_issue_tracking(parent_number, next_sub_issue_numbers)
|
|
407
|
+
stale_numbers = sub_issues(parent_number) - next_sub_issue_numbers
|
|
408
|
+
stale_numbers.each do |sub_number|
|
|
409
|
+
hierarchy = hierarchies[sub_number.to_s]
|
|
410
|
+
next unless hierarchy
|
|
411
|
+
|
|
412
|
+
hierarchy.delete("parent")
|
|
413
|
+
hierarchy.delete("dependencies")
|
|
414
|
+
hierarchy.delete("dependencies_updated_at")
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
public
|
|
419
|
+
|
|
388
420
|
def blocking_status(issue_number)
|
|
389
|
-
|
|
421
|
+
dependency_numbers = issue_dependencies(issue_number)
|
|
422
|
+
if dependency_numbers.any?
|
|
423
|
+
return {
|
|
424
|
+
blocked: true,
|
|
425
|
+
blockers: dependency_numbers,
|
|
426
|
+
blocker_count: dependency_numbers.size
|
|
427
|
+
}
|
|
428
|
+
end
|
|
429
|
+
|
|
390
430
|
sub_issue_numbers = sub_issues(issue_number)
|
|
391
431
|
return {blocked: false, blockers: []} if sub_issue_numbers.empty?
|
|
392
432
|
|