ace-task 0.31.9 → 0.36.7
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/CHANGELOG.md +204 -0
- data/README.md +7 -0
- data/handbook/guides/task-definition.g.md +47 -13
- data/handbook/skills/as-task-draft/SKILL.md +7 -1
- data/handbook/skills/as-task-plan/SKILL.md +6 -1
- data/handbook/skills/as-task-work/SKILL.md +6 -1
- data/handbook/templates/task/draft.template.md +6 -6
- data/handbook/workflow-instructions/bug/analyze.wf.md +40 -8
- data/handbook/workflow-instructions/bug/fix.wf.md +15 -2
- data/handbook/workflow-instructions/task/draft.wf.md +44 -16
- data/handbook/workflow-instructions/task/plan.wf.md +12 -1
- data/handbook/workflow-instructions/task/review.wf.md +21 -14
- data/handbook/workflow-instructions/task/work.wf.md +15 -11
- data/lib/ace/task/atoms/task_frontmatter_defaults.rb +12 -1
- data/lib/ace/task/atoms/task_validation_rules.rb +1 -1
- data/lib/ace/task/cli/commands/create.rb +46 -4
- data/lib/ace/task/cli/commands/github_sync.rb +65 -0
- data/lib/ace/task/cli/commands/plan.rb +8 -4
- data/lib/ace/task/cli.rb +7 -2
- data/lib/ace/task/molecules/github_issue_sync_adapter.rb +77 -0
- data/lib/ace/task/molecules/subtask_creator.rb +12 -2
- data/lib/ace/task/molecules/task_creator.rb +75 -35
- data/lib/ace/task/molecules/task_frontmatter_validator.rb +15 -0
- data/lib/ace/task/molecules/task_plan_generator.rb +13 -1
- data/lib/ace/task/organisms/task_doctor.rb +31 -1
- data/lib/ace/task/organisms/task_manager.rb +193 -25
- data/lib/ace/task/version.rb +1 -1
- data/lib/ace/task.rb +2 -0
- metadata +18 -2
|
@@ -7,6 +7,7 @@ require_relative "../molecules/task_loader"
|
|
|
7
7
|
require_relative "../molecules/task_creator"
|
|
8
8
|
require_relative "../molecules/subtask_creator"
|
|
9
9
|
require_relative "../molecules/task_reparenter"
|
|
10
|
+
require_relative "../molecules/github_issue_sync_adapter"
|
|
10
11
|
require_relative "../atoms/task_validation_rules"
|
|
11
12
|
|
|
12
13
|
module Ace
|
|
@@ -15,6 +16,9 @@ module Ace
|
|
|
15
16
|
# Orchestrates all task CRUD operations.
|
|
16
17
|
# Entry point for task management with config-driven root directory.
|
|
17
18
|
class TaskManager
|
|
19
|
+
CREATE_RETRY_LIMIT = 3
|
|
20
|
+
class CreateRetriesExhaustedError < StandardError; end
|
|
21
|
+
|
|
18
22
|
attr_reader :last_list_total, :last_folder_counts
|
|
19
23
|
|
|
20
24
|
# @param root_dir [String, nil] Override root directory for tasks
|
|
@@ -35,11 +39,41 @@ module Ace
|
|
|
35
39
|
# @param dependencies [Array<String>] Dependency task IDs
|
|
36
40
|
# @param use_llm_slug [Boolean] Whether to attempt LLM slug generation
|
|
37
41
|
# @return [Models::Task] Created task
|
|
38
|
-
def create(
|
|
42
|
+
def create(
|
|
43
|
+
title,
|
|
44
|
+
status: nil,
|
|
45
|
+
priority: nil,
|
|
46
|
+
tags: [],
|
|
47
|
+
dependencies: [],
|
|
48
|
+
use_llm_slug: false,
|
|
49
|
+
estimate: nil,
|
|
50
|
+
github_issue: nil
|
|
51
|
+
)
|
|
39
52
|
ensure_root_dir
|
|
53
|
+
ensure_github_issue_linkable!(github_issue)
|
|
40
54
|
creator = Molecules::TaskCreator.new(root_dir: @root_dir, config: @config)
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
attempts = 0
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
attempts += 1
|
|
59
|
+
created_task = creator.create(
|
|
60
|
+
title,
|
|
61
|
+
status: status,
|
|
62
|
+
priority: priority,
|
|
63
|
+
tags: tags,
|
|
64
|
+
dependencies: dependencies,
|
|
65
|
+
use_llm_slug: use_llm_slug,
|
|
66
|
+
time: Time.now.utc + ((attempts - 1) * 2),
|
|
67
|
+
estimate: estimate,
|
|
68
|
+
github_issue: github_issue
|
|
69
|
+
)
|
|
70
|
+
sync_linked_issues_for(created_task, reason: "create")
|
|
71
|
+
created_task
|
|
72
|
+
rescue Molecules::TaskCreator::IdCollisionError
|
|
73
|
+
retry if attempts < CREATE_RETRY_LIMIT
|
|
74
|
+
raise CreateRetriesExhaustedError,
|
|
75
|
+
"Failed to create task: unable to generate a unique ID after #{CREATE_RETRY_LIMIT} attempts"
|
|
76
|
+
end
|
|
43
77
|
end
|
|
44
78
|
|
|
45
79
|
# Show (load) a single task by reference, including subtasks.
|
|
@@ -109,6 +143,8 @@ module Ace
|
|
|
109
143
|
|
|
110
144
|
# Apply field updates if any
|
|
111
145
|
has_field_updates = [set, add, remove].any? { |h| h && !h.empty? }
|
|
146
|
+
desired_issue = extract_desired_github_issue(task, set: set, remove: remove)
|
|
147
|
+
ensure_github_issue_linkable!(desired_issue, previous_task: task) if desired_issue
|
|
112
148
|
if has_field_updates
|
|
113
149
|
Ace::Support::Items::Molecules::FieldUpdater.update(
|
|
114
150
|
task.file_path, set: set, add: add, remove: remove
|
|
@@ -146,7 +182,9 @@ module Ace
|
|
|
146
182
|
resolve_fn = ->(r) { show(r) }
|
|
147
183
|
# Reload task from current path before reparenting (may have been field-updated)
|
|
148
184
|
task_for_reparent = loader.load(current_path, id: task.id, special_folder: current_special)
|
|
149
|
-
|
|
185
|
+
reparented = reparenter.reparent(task_for_reparent, target: move_as_child_of, resolve_ref: resolve_fn)
|
|
186
|
+
sync_linked_issues_for(reparented, reason: "reparent", previous_task: task)
|
|
187
|
+
return reparented
|
|
150
188
|
end
|
|
151
189
|
|
|
152
190
|
# Auto-archive hook: if a subtask status was set to terminal,
|
|
@@ -156,7 +194,11 @@ module Ace
|
|
|
156
194
|
end
|
|
157
195
|
|
|
158
196
|
# Reload and return updated task
|
|
159
|
-
loader.load(current_path, id: current_id, special_folder: current_special)
|
|
197
|
+
updated_task = loader.load(current_path, id: current_id, special_folder: current_special)
|
|
198
|
+
if sync_needed_after_update?(task, updated_task, set: set, add: add, remove: remove, move_to: move_to)
|
|
199
|
+
sync_linked_issues_for(updated_task, reason: "update", previous_task: task)
|
|
200
|
+
end
|
|
201
|
+
updated_task
|
|
160
202
|
end
|
|
161
203
|
|
|
162
204
|
# Create a subtask within a parent task.
|
|
@@ -166,12 +208,45 @@ module Ace
|
|
|
166
208
|
# @param priority [String, nil] Priority level
|
|
167
209
|
# @param tags [Array<String>] Tags
|
|
168
210
|
# @return [Models::Task, nil] Created subtask or nil if parent not found
|
|
169
|
-
def create_subtask(parent_ref, title, status: nil, priority: nil, tags: [], estimate: nil)
|
|
211
|
+
def create_subtask(parent_ref, title, status: nil, priority: nil, tags: [], estimate: nil, github_issue: nil)
|
|
170
212
|
parent = show(parent_ref)
|
|
171
213
|
return nil unless parent
|
|
172
214
|
|
|
215
|
+
ensure_github_issue_linkable!(github_issue)
|
|
173
216
|
subtask_creator = Molecules::SubtaskCreator.new(config: @config)
|
|
174
|
-
subtask_creator.create(
|
|
217
|
+
created_subtask = subtask_creator.create(
|
|
218
|
+
parent,
|
|
219
|
+
title,
|
|
220
|
+
status: status,
|
|
221
|
+
priority: priority,
|
|
222
|
+
tags: tags,
|
|
223
|
+
estimate: estimate,
|
|
224
|
+
github_issue: github_issue
|
|
225
|
+
)
|
|
226
|
+
sync_linked_issues_for(created_subtask, reason: "create")
|
|
227
|
+
created_subtask
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def github_sync(ref: nil, all: false)
|
|
231
|
+
raise ArgumentError, "Provide --all or a task reference" if !all && (ref.nil? || ref.strip.empty?)
|
|
232
|
+
|
|
233
|
+
if all
|
|
234
|
+
tasks = list(in_folder: "all")
|
|
235
|
+
linked_tasks = tasks.select { |t| linked_issue_id(t) }
|
|
236
|
+
results = linked_tasks.map { |task| sync_linked_issues_for(task, reason: "manual-sync") }
|
|
237
|
+
return summarize_manual_sync_results(results, skipped: tasks.length - linked_tasks.length)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
task = show(ref)
|
|
241
|
+
return nil unless task
|
|
242
|
+
|
|
243
|
+
unless linked_issue_id(task)
|
|
244
|
+
return {synced: 0, failed: 0, skipped: 1, task_id: task.id, failures: []}
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
result = sync_linked_issues_for(task, reason: "manual-sync")
|
|
248
|
+
summary = summarize_manual_sync_results([result], skipped: 0)
|
|
249
|
+
summary.merge(task_id: task.id)
|
|
175
250
|
end
|
|
176
251
|
|
|
177
252
|
# Get the root directory.
|
|
@@ -261,10 +336,10 @@ module Ace
|
|
|
261
336
|
parent_dir, recursive: true
|
|
262
337
|
)
|
|
263
338
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
339
|
+
parent = load_parent_from_directory(parent_dir, loader)
|
|
340
|
+
return unless parent
|
|
341
|
+
|
|
342
|
+
archive_parent_via_update(parent)
|
|
268
343
|
end
|
|
269
344
|
|
|
270
345
|
def parse_archive_date(task)
|
|
@@ -312,27 +387,46 @@ module Ace
|
|
|
312
387
|
}
|
|
313
388
|
end
|
|
314
389
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
mover = Ace::Support::Items::Molecules::FolderMover.new(@root_dir)
|
|
323
|
-
new_parent_path = mover.move(parent, to: "archive", date: parse_archive_date(parent))
|
|
324
|
-
new_special_folder = Ace::Support::Items::Atoms::SpecialFolderDetector.detect_in_path(
|
|
325
|
-
new_parent_path, root: @root_dir
|
|
326
|
-
)
|
|
390
|
+
archived_parent = archive_parent_via_update(parent)
|
|
391
|
+
return {
|
|
392
|
+
path: task.path,
|
|
393
|
+
special_folder: task.special_folder,
|
|
394
|
+
id: task.id
|
|
395
|
+
} unless archived_parent
|
|
327
396
|
|
|
328
397
|
@last_update_note = "Archived parent task #{parent.id} because all subtasks are terminal."
|
|
329
398
|
{
|
|
330
|
-
path: File.join(
|
|
331
|
-
special_folder:
|
|
399
|
+
path: File.join(archived_parent.path, File.basename(task.path)),
|
|
400
|
+
special_folder: archived_parent.special_folder,
|
|
332
401
|
id: task.id
|
|
333
402
|
}
|
|
334
403
|
end
|
|
335
404
|
|
|
405
|
+
def archive_parent_via_update(parent)
|
|
406
|
+
previous_note = @last_update_note
|
|
407
|
+
archived_parent = update(
|
|
408
|
+
parent.id,
|
|
409
|
+
set: {"status" => "done"},
|
|
410
|
+
move_to: "archive"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if archived_parent && @last_update_note == previous_note
|
|
414
|
+
@last_update_note = "Archived parent task #{parent.id} because all subtasks are terminal."
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
archived_parent
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def load_parent_from_directory(parent_dir, loader)
|
|
421
|
+
parent_id = File.basename(parent_dir).split("-", 2).first
|
|
422
|
+
return nil unless parent_id
|
|
423
|
+
|
|
424
|
+
parent_special = Ace::Support::Items::Atoms::SpecialFolderDetector.detect_in_path(
|
|
425
|
+
parent_dir, root: @root_dir
|
|
426
|
+
)
|
|
427
|
+
loader.load(parent_dir, id: parent_id, special_folder: parent_special)
|
|
428
|
+
end
|
|
429
|
+
|
|
336
430
|
# Value accessor for FilterApplier
|
|
337
431
|
def task_value_accessor(item, key)
|
|
338
432
|
case key
|
|
@@ -347,6 +441,80 @@ module Ace
|
|
|
347
441
|
item.metadata[key] || item.metadata[key.to_sym] if item.respond_to?(:metadata) && item.metadata
|
|
348
442
|
end
|
|
349
443
|
end
|
|
444
|
+
|
|
445
|
+
def sync_needed_after_update?(before_task, after_task, set:, add:, remove:, move_to:)
|
|
446
|
+
return false unless after_task
|
|
447
|
+
return true if move_to
|
|
448
|
+
return true if before_task.path != after_task.path
|
|
449
|
+
return true if linked_issue_id(before_task) != linked_issue_id(after_task)
|
|
450
|
+
|
|
451
|
+
touched_keys = [set, add, remove].compact.flat_map(&:keys).map(&:to_s)
|
|
452
|
+
touched_keys.any? do |key|
|
|
453
|
+
key == "title" || key == "status" || key == "github_issue"
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def linked_issue_id(task)
|
|
458
|
+
return nil unless task&.metadata
|
|
459
|
+
|
|
460
|
+
issue_id = task.metadata["github_issue"]
|
|
461
|
+
return nil unless issue_id.to_i.positive?
|
|
462
|
+
|
|
463
|
+
issue_id.to_i
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def extract_desired_github_issue(task, set:, remove:)
|
|
467
|
+
return nil if set&.key?("github_issue") && !set["github_issue"].to_i.positive?
|
|
468
|
+
return set["github_issue"].to_i if set&.key?("github_issue") && set["github_issue"].to_i.positive?
|
|
469
|
+
return nil if Array(remove&.keys).map(&:to_s).include?("github_issue")
|
|
470
|
+
|
|
471
|
+
linked_issue_id(task)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def ensure_github_issue_linkable!(github_issue, previous_task: nil)
|
|
475
|
+
return unless github_issue
|
|
476
|
+
|
|
477
|
+
Molecules::GithubIssueSyncAdapter.new.validate_link!(issue_id: github_issue, previous_task: previous_task)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def sync_linked_issues_for(task, reason:, previous_task: nil)
|
|
481
|
+
issue_ids = [linked_issue_id(task), linked_issue_id(previous_task)].compact.uniq
|
|
482
|
+
return sync_result_for(task: task, issues: issue_ids, success: true, reason: reason) if issue_ids.empty?
|
|
483
|
+
|
|
484
|
+
adapter = Molecules::GithubIssueSyncAdapter.new
|
|
485
|
+
adapter.sync_task(task: task, reason: reason, previous_task: previous_task)
|
|
486
|
+
sync_result_for(task: task, issues: issue_ids, success: true, reason: reason)
|
|
487
|
+
rescue StandardError => e
|
|
488
|
+
@last_update_note = "GitHub sync warning for task #{task&.id}: #{e.message}"
|
|
489
|
+
sync_result_for(task: task, issues: issue_ids, success: false, reason: reason, error: e.message)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def sync_result_for(task:, issues:, success:, reason:, error: nil)
|
|
493
|
+
{
|
|
494
|
+
task_id: task&.id,
|
|
495
|
+
issue_ids: issues,
|
|
496
|
+
success: success,
|
|
497
|
+
reason: reason,
|
|
498
|
+
error: error
|
|
499
|
+
}
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def summarize_manual_sync_results(results, skipped:)
|
|
503
|
+
failures = results.reject { |result| result[:success] }.map do |result|
|
|
504
|
+
{
|
|
505
|
+
task_id: result[:task_id],
|
|
506
|
+
issue_ids: result[:issue_ids],
|
|
507
|
+
error: result[:error]
|
|
508
|
+
}
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
{
|
|
512
|
+
synced: results.length - failures.length,
|
|
513
|
+
failed: failures.length,
|
|
514
|
+
skipped: skipped,
|
|
515
|
+
failures: failures
|
|
516
|
+
}
|
|
517
|
+
end
|
|
350
518
|
end
|
|
351
519
|
end
|
|
352
520
|
end
|
data/lib/ace/task/version.rb
CHANGED
data/lib/ace/task.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "task/version"
|
|
|
5
5
|
# Dependencies
|
|
6
6
|
require "ace/support/items"
|
|
7
7
|
require "ace/b36ts"
|
|
8
|
+
require "ace/git"
|
|
8
9
|
|
|
9
10
|
# Atoms
|
|
10
11
|
require_relative "task/atoms/task_id_formatter"
|
|
@@ -23,6 +24,7 @@ require_relative "task/molecules/task_creator"
|
|
|
23
24
|
require_relative "task/molecules/subtask_creator"
|
|
24
25
|
require_relative "task/molecules/task_display_formatter"
|
|
25
26
|
require_relative "task/molecules/path_utils"
|
|
27
|
+
require_relative "task/molecules/github_issue_sync_adapter"
|
|
26
28
|
require_relative "task/molecules/task_plan_cache"
|
|
27
29
|
require_relative "task/molecules/task_plan_generator"
|
|
28
30
|
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-task
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.36.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-26 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-core
|
|
@@ -79,6 +79,20 @@ dependencies:
|
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '0.13'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: ace-git
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.19'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.19'
|
|
82
96
|
- !ruby/object:Gem::Dependency
|
|
83
97
|
name: ace-support-cli
|
|
84
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -144,12 +158,14 @@ files:
|
|
|
144
158
|
- lib/ace/task/cli.rb
|
|
145
159
|
- lib/ace/task/cli/commands/create.rb
|
|
146
160
|
- lib/ace/task/cli/commands/doctor.rb
|
|
161
|
+
- lib/ace/task/cli/commands/github_sync.rb
|
|
147
162
|
- lib/ace/task/cli/commands/list.rb
|
|
148
163
|
- lib/ace/task/cli/commands/plan.rb
|
|
149
164
|
- lib/ace/task/cli/commands/show.rb
|
|
150
165
|
- lib/ace/task/cli/commands/status.rb
|
|
151
166
|
- lib/ace/task/cli/commands/update.rb
|
|
152
167
|
- lib/ace/task/models/task.rb
|
|
168
|
+
- lib/ace/task/molecules/github_issue_sync_adapter.rb
|
|
153
169
|
- lib/ace/task/molecules/path_utils.rb
|
|
154
170
|
- lib/ace/task/molecules/subtask_creator.rb
|
|
155
171
|
- lib/ace/task/molecules/task_config_loader.rb
|