Class: Ace::Task::Organisms::TaskManager

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/task/organisms/task_manager.rb

Overview

Orchestrates all task CRUD operations. Entry point for task management with config-driven root directory.

Defined Under Namespace

Classes: CreateRetriesExhaustedError

Constant Summary collapse

CREATE_RETRY_LIMIT =
3

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root_dir: nil, config: nil) ⇒ TaskManager

Returns a new instance of TaskManager.

Parameters:

  • root_dir (String, nil) (defaults to: nil)

    Override root directory for tasks

  • config (Hash, nil) (defaults to: nil)

    Override configuration



26
27
28
29
30
# File 'lib/ace/task/organisms/task_manager.rb', line 26

def initialize(root_dir: nil, config: nil)
  @config = config || load_config
  @root_dir = root_dir || resolve_root_dir
  @last_update_note = nil
end

Instance Attribute Details

#last_folder_countsObject (readonly)

Returns the value of attribute last_folder_counts.



22
23
24
# File 'lib/ace/task/organisms/task_manager.rb', line 22

def last_folder_counts
  @last_folder_counts
end

#last_list_totalObject (readonly)

Returns the value of attribute last_list_total.



22
23
24
# File 'lib/ace/task/organisms/task_manager.rb', line 22

def last_list_total
  @last_list_total
end

#last_update_noteObject (readonly)

Returns the value of attribute last_update_note.



32
33
34
# File 'lib/ace/task/organisms/task_manager.rb', line 32

def last_update_note
  @last_update_note
end

#root_dirString (readonly)

Get the root directory.

Returns:

  • (String)

    Absolute path to tasks root



254
255
256
# File 'lib/ace/task/organisms/task_manager.rb', line 254

def root_dir
  @root_dir
end

Instance Method Details

#create(title, status: nil, priority: nil, tags: [], dependencies: [], use_llm_slug: false, estimate: nil, github_issue: nil) ⇒ Models::Task

Create a new task.

Parameters:

  • title (String)

    Task title

  • status (String, nil) (defaults to: nil)

    Initial status

  • priority (String, nil) (defaults to: nil)

    Priority level

  • tags (Array<String>) (defaults to: [])

    Tags

  • dependencies (Array<String>) (defaults to: [])

    Dependency task IDs

  • use_llm_slug (Boolean) (defaults to: false)

    Whether to attempt LLM slug generation

Returns:



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/ace/task/organisms/task_manager.rb', line 42

def create(
  title,
  status: nil,
  priority: nil,
  tags: [],
  dependencies: [],
  use_llm_slug: false,
  estimate: nil,
  github_issue: nil
)
  ensure_root_dir
  ensure_github_issue_linkable!(github_issue)
  creator = Molecules::TaskCreator.new(root_dir: @root_dir, config: @config)
  attempts = 0

  begin
    attempts += 1
    created_task = creator.create(
      title,
      status: status,
      priority: priority,
      tags: tags,
      dependencies: dependencies,
      use_llm_slug: use_llm_slug,
      time: Time.now.utc + ((attempts - 1) * 2),
      estimate: estimate,
      github_issue: github_issue
    )
    sync_linked_issues_for(created_task, reason: "create")
    created_task
  rescue Molecules::TaskCreator::IdCollisionError
    retry if attempts < CREATE_RETRY_LIMIT
    raise CreateRetriesExhaustedError,
      "Failed to create task: unable to generate a unique ID after #{CREATE_RETRY_LIMIT} attempts"
  end
end

#create_subtask(parent_ref, title, status: nil, priority: nil, tags: [], estimate: nil, github_issue: nil) ⇒ Models::Task?

Create a subtask within a parent task.

Parameters:

  • parent_ref (String)

    Parent task reference

  • title (String)

    Subtask title

  • status (String, nil) (defaults to: nil)

    Initial status

  • priority (String, nil) (defaults to: nil)

    Priority level

  • tags (Array<String>) (defaults to: [])

    Tags

Returns:

  • (Models::Task, nil)

    Created subtask or nil if parent not found



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/ace/task/organisms/task_manager.rb', line 211

def create_subtask(parent_ref, title, status: nil, priority: nil, tags: [], estimate: nil, github_issue: nil)
  parent = show(parent_ref)
  return nil unless parent

  ensure_github_issue_linkable!(github_issue)
  subtask_creator = Molecules::SubtaskCreator.new(config: @config)
  created_subtask = subtask_creator.create(
    parent,
    title,
    status: status,
    priority: priority,
    tags: tags,
    estimate: estimate,
    github_issue: github_issue
  )
  sync_linked_issues_for(created_subtask, reason: "create")
  created_subtask
end

#github_sync(ref: nil, all: false) ⇒ Object

Raises:

  • (ArgumentError)


230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/ace/task/organisms/task_manager.rb', line 230

def github_sync(ref: nil, all: false)
  raise ArgumentError, "Provide --all or a task reference" if !all && (ref.nil? || ref.strip.empty?)

  if all
    tasks = list(in_folder: "all")
    linked_tasks = tasks.select { |t| linked_issue_id(t) }
    results = linked_tasks.map { |task| sync_linked_issues_for(task, reason: "manual-sync") }
    return summarize_manual_sync_results(results, skipped: tasks.length - linked_tasks.length)
  end

  task = show(ref)
  return nil unless task

  unless linked_issue_id(task)
    return {synced: 0, failed: 0, skipped: 1, task_id: task.id, failures: []}
  end

  result = sync_linked_issues_for(task, reason: "manual-sync")
  summary = summarize_manual_sync_results([result], skipped: 0)
  summary.merge(task_id: task.id)
end

#list(status: nil, in_folder: "next", tags: [], filters: nil, sort: "smart") ⇒ Array<Models::Task>

List tasks with optional filtering and sorting.

Parameters:

  • status (String, nil) (defaults to: nil)

    Filter by status

  • in_folder (String, nil) (defaults to: "next")

    Filter by special folder (default: “next” = root items only)

  • tags (Array<String>) (defaults to: [])

    Filter by tags (any match)

  • filters (Array<String>, nil) (defaults to: nil)

    Generic filter strings

  • sort (String) (defaults to: "smart")

    Sort order: “smart”, “id”, “priority”, “created” (default: “smart”)

Returns:



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/ace/task/organisms/task_manager.rb', line 99

def list(status: nil, in_folder: "next", tags: [], filters: nil, sort: "smart")
  scanner = Molecules::TaskScanner.new(@root_dir)
  scan_results = scanner.scan_in_folder(in_folder)
  @last_list_total = scanner.last_scan_total
  @last_folder_counts = scanner.last_folder_counts

  loader = Molecules::TaskLoader.new
  tasks = scan_results.filter_map do |sr|
    loader.load(sr.dir_path, id: sr.id, special_folder: sr.special_folder)
  end

  # Apply legacy filters
  tasks = tasks.select { |t| t.status == status } if status
  tasks = filter_by_tags(tasks, tags) if tags.any?

  # Apply generic --filter specs
  if filters && !filters.empty?
    filter_specs = Ace::Support::Items::Atoms::FilterParser.parse(filters)
    tasks = Ace::Support::Items::Molecules::FilterApplier.apply(
      tasks, filter_specs, value_accessor: method(:task_value_accessor)
    )
  end

  apply_sort(tasks, sort)
end

#show(ref) ⇒ Models::Task?

Show (load) a single task by reference, including subtasks.

Parameters:

  • ref (String)

    Full ID, shortcut, or subtask reference

Returns:



82
83
84
85
86
87
88
89
90
# File 'lib/ace/task/organisms/task_manager.rb', line 82

def show(ref)
  scan_result = resolve_scan_result(ref)
  return nil unless scan_result

  loader = Molecules::TaskLoader.new
  loader.load(scan_result.dir_path,
    id: scan_result.id,
    special_folder: scan_result.special_folder)
end

#update(ref, set: {}, add: {}, remove: {}, move_to: nil, move_as_child_of: nil) ⇒ Models::Task?

Update a task’s frontmatter fields and optionally move to a folder.

Parameters:

  • ref (String)

    Task reference

  • set (Hash) (defaults to: {})

    Fields to set (supports dot-notation for nested keys)

  • add (Hash) (defaults to: {})

    Fields to add to arrays

  • remove (Hash) (defaults to: {})

    Fields to remove from arrays

  • move_to (String, nil) (defaults to: nil)

    Target folder to move to (archive, maybe, anytime, next/root//)

  • move_as_child_of (String, nil) (defaults to: nil)

    Reparent: parent ref, “none” (promote), “self” (orchestrator)

Returns:



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/ace/task/organisms/task_manager.rb', line 133

def update(ref, set: {}, add: {}, remove: {}, move_to: nil, move_as_child_of: nil)
  @last_update_note = nil
  scan_result = resolve_scan_result(ref)
  return nil unless scan_result

  loader = Molecules::TaskLoader.new
  task = loader.load(scan_result.dir_path,
    id: scan_result.id,
    special_folder: scan_result.special_folder)
  return nil unless task

  # Apply field updates if any
  has_field_updates = [set, add, remove].any? { |h| h && !h.empty? }
  desired_issue = extract_desired_github_issue(task, set: set, remove: remove)
  ensure_github_issue_linkable!(desired_issue, previous_task: task) if desired_issue
  if has_field_updates
    Ace::Support::Items::Molecules::FieldUpdater.update(
      task.file_path, set: set, add: add, remove: remove
    )
  end

  # Apply move if requested
  current_path = task.path
  current_special = task.special_folder
  current_id = task.id
  if move_to
    if archive_move_for_subtask?(task, move_to)
      result = handle_subtask_archive_move(task, loader)
      current_path = result[:path]
      current_special = result[:special_folder]
      current_id = result[:id]
    else
      mover = Ace::Support::Items::Molecules::FolderMover.new(@root_dir)
      new_path = if Ace::Support::Items::Atoms::SpecialFolderDetector.move_to_root?(move_to)
        mover.move_to_root(task)
      else
        archive_date = parse_archive_date(task)
        mover.move(task, to: move_to, date: archive_date)
      end
      current_path = new_path
      current_special = Ace::Support::Items::Atoms::SpecialFolderDetector.detect_in_path(
        new_path, root: @root_dir
      )
    end
  end

  # Reparent if requested (mutually exclusive with move_to)
  if move_as_child_of
    reparenter = Molecules::TaskReparenter.new(root_dir: @root_dir, config: @config)
    resolve_fn = ->(r) { show(r) }
    # Reload task from current path before reparenting (may have been field-updated)
    task_for_reparent = loader.load(current_path, id: task.id, special_folder: current_special)
    reparented = reparenter.reparent(task_for_reparent, target: move_as_child_of, resolve_ref: resolve_fn)
    sync_linked_issues_for(reparented, reason: "reparent", previous_task: task)
    return reparented
  end

  # Auto-archive hook: if a subtask status was set to terminal,
  # check if all siblings are terminal and auto-move parent to archive
  if set && set.key?("status")
    check_auto_archive(task, set["status"], loader)
  end

  # Reload and return updated task
  updated_task = loader.load(current_path, id: current_id, special_folder: current_special)
  if sync_needed_after_update?(task, updated_task, set: set, add: add, remove: remove, move_to: move_to)
    sync_linked_issues_for(updated_task, reason: "update", previous_task: task)
  end
  updated_task
end