ace-task 0.31.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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/nav/protocols/skill-sources/ace-task.yml +19 -0
  3. data/.ace-defaults/nav/protocols/wfi-sources/ace-task.yml +19 -0
  4. data/.ace-defaults/task/config.yml +25 -0
  5. data/CHANGELOG.md +518 -0
  6. data/README.md +52 -0
  7. data/Rakefile +12 -0
  8. data/exe/ace-task +22 -0
  9. data/handbook/guides/task-definition.g.md +156 -0
  10. data/handbook/skills/as-bug-analyze/SKILL.md +26 -0
  11. data/handbook/skills/as-bug-fix/SKILL.md +27 -0
  12. data/handbook/skills/as-task-document-unplanned/SKILL.md +27 -0
  13. data/handbook/skills/as-task-draft/SKILL.md +24 -0
  14. data/handbook/skills/as-task-finder/SKILL.md +27 -0
  15. data/handbook/skills/as-task-plan/SKILL.md +30 -0
  16. data/handbook/skills/as-task-review/SKILL.md +25 -0
  17. data/handbook/skills/as-task-review-questions/SKILL.md +25 -0
  18. data/handbook/skills/as-task-update/SKILL.md +21 -0
  19. data/handbook/skills/as-task-work/SKILL.md +41 -0
  20. data/handbook/templates/task/draft.template.md +166 -0
  21. data/handbook/templates/task/file-modification-checklist.template.md +26 -0
  22. data/handbook/templates/task/technical-approach.template.md +26 -0
  23. data/handbook/workflow-instructions/bug/analyze.wf.md +458 -0
  24. data/handbook/workflow-instructions/bug/fix.wf.md +512 -0
  25. data/handbook/workflow-instructions/task/document-unplanned.wf.md +222 -0
  26. data/handbook/workflow-instructions/task/draft.wf.md +552 -0
  27. data/handbook/workflow-instructions/task/finder.wf.md +22 -0
  28. data/handbook/workflow-instructions/task/plan.wf.md +489 -0
  29. data/handbook/workflow-instructions/task/review-plan.wf.md +144 -0
  30. data/handbook/workflow-instructions/task/review-questions.wf.md +411 -0
  31. data/handbook/workflow-instructions/task/review-work.wf.md +146 -0
  32. data/handbook/workflow-instructions/task/review.wf.md +351 -0
  33. data/handbook/workflow-instructions/task/update.wf.md +118 -0
  34. data/handbook/workflow-instructions/task/work.wf.md +106 -0
  35. data/lib/ace/task/atoms/task_file_pattern.rb +68 -0
  36. data/lib/ace/task/atoms/task_frontmatter_defaults.rb +46 -0
  37. data/lib/ace/task/atoms/task_id_formatter.rb +62 -0
  38. data/lib/ace/task/atoms/task_validation_rules.rb +51 -0
  39. data/lib/ace/task/cli/commands/create.rb +105 -0
  40. data/lib/ace/task/cli/commands/doctor.rb +206 -0
  41. data/lib/ace/task/cli/commands/list.rb +73 -0
  42. data/lib/ace/task/cli/commands/plan.rb +119 -0
  43. data/lib/ace/task/cli/commands/show.rb +58 -0
  44. data/lib/ace/task/cli/commands/status.rb +77 -0
  45. data/lib/ace/task/cli/commands/update.rb +183 -0
  46. data/lib/ace/task/cli.rb +83 -0
  47. data/lib/ace/task/models/task.rb +46 -0
  48. data/lib/ace/task/molecules/path_utils.rb +20 -0
  49. data/lib/ace/task/molecules/subtask_creator.rb +130 -0
  50. data/lib/ace/task/molecules/task_config_loader.rb +92 -0
  51. data/lib/ace/task/molecules/task_creator.rb +115 -0
  52. data/lib/ace/task/molecules/task_display_formatter.rb +221 -0
  53. data/lib/ace/task/molecules/task_doctor_fixer.rb +510 -0
  54. data/lib/ace/task/molecules/task_doctor_reporter.rb +264 -0
  55. data/lib/ace/task/molecules/task_frontmatter_validator.rb +138 -0
  56. data/lib/ace/task/molecules/task_loader.rb +119 -0
  57. data/lib/ace/task/molecules/task_plan_cache.rb +190 -0
  58. data/lib/ace/task/molecules/task_plan_generator.rb +141 -0
  59. data/lib/ace/task/molecules/task_plan_prompt_builder.rb +91 -0
  60. data/lib/ace/task/molecules/task_reparenter.rb +247 -0
  61. data/lib/ace/task/molecules/task_resolver.rb +115 -0
  62. data/lib/ace/task/molecules/task_scanner.rb +129 -0
  63. data/lib/ace/task/molecules/task_structure_validator.rb +154 -0
  64. data/lib/ace/task/organisms/task_doctor.rb +199 -0
  65. data/lib/ace/task/organisms/task_manager.rb +353 -0
  66. data/lib/ace/task/version.rb +7 -0
  67. data/lib/ace/task.rb +37 -0
  68. metadata +197 -0
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "time"
5
+ require_relative "../atoms/task_id_formatter"
6
+ require_relative "../atoms/task_frontmatter_defaults"
7
+ require_relative "task_loader"
8
+
9
+ module Ace
10
+ module Task
11
+ module Molecules
12
+ # Creates new tasks with B36TS-based type-marked IDs.
13
+ # Generates folder structure and spec file with full frontmatter.
14
+ # Optionally uses LLM for slug generation with deterministic fallback.
15
+ class TaskCreator
16
+ # @param root_dir [String] Root directory for tasks
17
+ # @param config [Hash] Configuration hash
18
+ def initialize(root_dir:, config: {})
19
+ @root_dir = root_dir
20
+ @config = config
21
+ end
22
+
23
+ # Create a new task
24
+ # @param title [String] Task title
25
+ # @param status [String] Initial status (default: from config or "pending")
26
+ # @param priority [String, nil] Priority level
27
+ # @param tags [Array<String>] Tags
28
+ # @param dependencies [Array<String>] Dependency task IDs
29
+ # @param time [Time] Creation time (default: now)
30
+ # @param use_llm_slug [Boolean] Whether to attempt LLM slug generation
31
+ # @return [Models::Task] Created task
32
+ def create(title, status: nil, priority: nil, tags: [], dependencies: [], time: Time.now.utc, use_llm_slug: false, estimate: nil)
33
+ raise ArgumentError, "Title is required" if title.nil? || title.strip.empty?
34
+
35
+ # Generate task ID
36
+ item_id = Atoms::TaskIdFormatter.generate(time)
37
+ formatted_id = item_id.formatted_id
38
+
39
+ # Generate slugs
40
+ slugs = if use_llm_slug
41
+ generate_llm_slugs(title) || generate_slugs(title)
42
+ else
43
+ generate_slugs(title)
44
+ end
45
+ folder_slug = slugs[:folder]
46
+ file_slug = slugs[:file]
47
+
48
+ # Create folder with folder_slug
49
+ folder_name = Atoms::TaskIdFormatter.folder_name(formatted_id, folder_slug)
50
+ task_dir = File.join(@root_dir, folder_name)
51
+ FileUtils.mkdir_p(task_dir)
52
+
53
+ # Build frontmatter with all fields
54
+ effective_status = status || @config.dig("task", "default_status") || "pending"
55
+ frontmatter = Atoms::TaskFrontmatterDefaults.build(
56
+ id: formatted_id,
57
+ status: effective_status,
58
+ priority: priority,
59
+ tags: tags,
60
+ dependencies: dependencies,
61
+ created_at: time,
62
+ estimate: estimate
63
+ )
64
+
65
+ # Write spec file with file_slug
66
+ spec_filename = Atoms::TaskIdFormatter.spec_filename(formatted_id, file_slug)
67
+ spec_file = File.join(task_dir, spec_filename)
68
+ content = build_spec_content(frontmatter: frontmatter, title: title)
69
+ File.write(spec_file, content)
70
+
71
+ # Load and return the created task
72
+ loader = TaskLoader.new
73
+ loader.load(task_dir, id: formatted_id)
74
+ end
75
+
76
+ private
77
+
78
+ def generate_folder_slug(title)
79
+ sanitized = Ace::Support::Items::Atoms::SlugSanitizer.sanitize(title)
80
+ words = sanitized.split("-")
81
+ result = words.take(5).join("-")
82
+ result.empty? ? "task" : result
83
+ end
84
+
85
+ def generate_file_slug(title)
86
+ sanitized = Ace::Support::Items::Atoms::SlugSanitizer.sanitize(title)
87
+ words = sanitized.split("-")
88
+ result = words.take(7).join("-")
89
+ result.empty? ? "task" : result
90
+ end
91
+
92
+ def generate_slugs(title)
93
+ {folder: generate_folder_slug(title), file: generate_file_slug(title)}
94
+ end
95
+
96
+ def generate_llm_slugs(title)
97
+ generator = Ace::Support::Items::Molecules::LlmSlugGenerator.new
98
+ result = generator.generate_task_slugs(title)
99
+ return nil unless result[:success]
100
+
101
+ folder_slug = result[:folder_slug] || generate_folder_slug(title)
102
+ file_slug = result[:file_slug] || generate_file_slug(title)
103
+ {folder: folder_slug, file: file_slug}
104
+ rescue
105
+ nil
106
+ end
107
+
108
+ def build_spec_content(frontmatter:, title:)
109
+ serialized = Ace::Support::Items::Atoms::FrontmatterSerializer.serialize(frontmatter)
110
+ "#{serialized}\n\n# #{title}\n"
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Task
5
+ module Molecules
6
+ # Formats task objects for terminal display.
7
+ # Handles single-task show output and compact list output,
8
+ # including subtask tree rendering.
9
+ class TaskDisplayFormatter
10
+ STATUS_SYMBOLS = {
11
+ "pending" => "○",
12
+ "in-progress" => "▶",
13
+ "done" => "✓",
14
+ "blocked" => "✗",
15
+ "draft" => "◇",
16
+ "skipped" => "–",
17
+ "cancelled" => "—"
18
+ }.freeze
19
+
20
+ STATUS_COLORS = {
21
+ "pending" => nil,
22
+ "in-progress" => Ace::Support::Items::Atoms::AnsiColors::YELLOW,
23
+ "done" => Ace::Support::Items::Atoms::AnsiColors::GREEN,
24
+ "blocked" => Ace::Support::Items::Atoms::AnsiColors::RED,
25
+ "draft" => Ace::Support::Items::Atoms::AnsiColors::CYAN,
26
+ "skipped" => Ace::Support::Items::Atoms::AnsiColors::DIM,
27
+ "cancelled" => Ace::Support::Items::Atoms::AnsiColors::DIM
28
+ }.freeze
29
+
30
+ PRIORITY_LABELS = {
31
+ "critical" => "▲",
32
+ "high" => "▲",
33
+ "medium" => "",
34
+ "low" => "▼"
35
+ }.freeze
36
+
37
+ PRIORITY_COLORS = {
38
+ "critical" => Ace::Support::Items::Atoms::AnsiColors::RED,
39
+ "low" => Ace::Support::Items::Atoms::AnsiColors::DIM
40
+ }.freeze
41
+
42
+ # Return the status symbol with ANSI color applied.
43
+ # @param status [String] Status string
44
+ # @return [String] Colored status symbol
45
+ def self.colored_status_sym(status)
46
+ sym = STATUS_SYMBOLS[status] || "○"
47
+ color = STATUS_COLORS[status]
48
+ color ? Ace::Support::Items::Atoms::AnsiColors.colorize(sym, color) : sym
49
+ end
50
+
51
+ private_class_method :colored_status_sym
52
+
53
+ # Format a single task for detailed display (show command).
54
+ # @param task [Models::Task] Task to format
55
+ # @param show_content [Boolean] Whether to include body content
56
+ # @return [String] Formatted output
57
+ def self.format(task, show_content: false)
58
+ lines = []
59
+
60
+ # Header line: status symbol, ID, title
61
+ status_sym = colored_status_sym(task.status)
62
+ priority_sym = PRIORITY_LABELS[task.priority] || ""
63
+ priority_prefix = priority_sym.empty? ? "" : "#{priority_sym} "
64
+ lines << "#{status_sym} #{priority_prefix}#{task.id} #{task.title}"
65
+
66
+ # Metadata line
67
+ meta_parts = []
68
+ meta_parts << "status: #{task.status}"
69
+ meta_parts << "priority: #{task.priority}" if task.priority
70
+ meta_parts << "estimate: #{task.estimate}" if task.estimate
71
+ lines << " #{meta_parts.join(" | ")}"
72
+
73
+ # Tags
74
+ if task.tags && task.tags.any?
75
+ lines << " tags: #{task.tags.join(", ")}"
76
+ end
77
+
78
+ # Dependencies
79
+ if task.dependencies && task.dependencies.any?
80
+ lines << " depends: #{task.dependencies.join(", ")}"
81
+ end
82
+
83
+ # Folder info
84
+ if task.special_folder
85
+ lines << " folder: #{task.special_folder}"
86
+ end
87
+
88
+ # Parent info for subtasks
89
+ if task.parent_id
90
+ lines << " parent: #{task.parent_id}"
91
+ end
92
+
93
+ # Subtasks
94
+ if task.has_subtasks?
95
+ lines << ""
96
+ lines << " Subtasks:"
97
+ task.subtasks.each do |st|
98
+ st_sym = colored_status_sym(st.status)
99
+ lines << " #{st_sym} #{st.id} #{st.title}"
100
+ end
101
+ end
102
+
103
+ # Body content
104
+ if show_content && task.content && !task.content.strip.empty?
105
+ lines << ""
106
+ lines << task.content.strip
107
+ end
108
+
109
+ lines.join("\n")
110
+ end
111
+
112
+ # Format a list of tasks for compact display (list command).
113
+ # @param tasks [Array<Models::Task>] Tasks to format
114
+ # @param total_count [Integer, nil] Total items before folder filtering
115
+ # @param global_folder_stats [Hash, nil] Folder name → count hash from full scan
116
+ # @return [String] Formatted list output
117
+ def self.format_list(tasks, total_count: nil, global_folder_stats: nil)
118
+ return "No tasks found." if tasks.empty?
119
+
120
+ lines = tasks.map { |task| format_list_item(task) }.join("\n")
121
+ "#{lines}\n\n#{format_stats_line(tasks, total_count: total_count, global_folder_stats: global_folder_stats)}"
122
+ end
123
+
124
+ STATUS_ORDER = %w[draft pending in-progress done blocked skipped cancelled].freeze
125
+
126
+ # Format a status overview with up-next, stats, and recently-done sections.
127
+ # @param categorized [Hash] Output of StatusCategorizer.categorize
128
+ # @param all_tasks [Array<Models::Task>] All tasks for stats computation
129
+ # @return [String] Formatted status output
130
+ def self.format_status(categorized, all_tasks:)
131
+ sections = []
132
+
133
+ # Up Next
134
+ sections << format_up_next_section(categorized[:up_next])
135
+
136
+ # Stats summary
137
+ sections << format_stats_line(all_tasks)
138
+
139
+ # Recently Done
140
+ sections << format_recently_done_section(categorized[:recently_done])
141
+
142
+ sections.join("\n\n")
143
+ end
144
+
145
+ # Format a stats summary line for a list of tasks.
146
+ # @param tasks [Array<Models::Task>] Tasks to summarize
147
+ # @param total_count [Integer, nil] Total items before folder filtering
148
+ # @param global_folder_stats [Hash, nil] Folder name → count hash from full scan
149
+ # @return [String] e.g. "Tasks: ○ 2 | ▶ 1 | ✓ 5 • 3 of 660"
150
+ def self.format_stats_line(tasks, total_count: nil, global_folder_stats: nil)
151
+ stats = Ace::Support::Items::Atoms::ItemStatistics.count_by(tasks, :status)
152
+ folder_stats = Ace::Support::Items::Atoms::ItemStatistics.count_by(tasks, :special_folder)
153
+ Ace::Support::Items::Atoms::StatsLineFormatter.format(
154
+ label: "Tasks",
155
+ stats: stats,
156
+ status_order: STATUS_ORDER,
157
+ status_icons: STATUS_SYMBOLS,
158
+ folder_stats: folder_stats,
159
+ total_count: total_count,
160
+ global_folder_stats: global_folder_stats
161
+ )
162
+ end
163
+
164
+ # Format a single task as a compact status line (id + title only).
165
+ # @param task [Models::Task] Task to format
166
+ # @return [String] e.g. " ○ 8pp.t.q7w Fix login bug"
167
+ def self.format_status_line(task)
168
+ status_sym = colored_status_sym(task.status)
169
+ " #{status_sym} #{task.id} #{task.title}"
170
+ end
171
+
172
+ private
173
+
174
+ # Format the "Up Next" section.
175
+ def self.format_up_next_section(up_next)
176
+ return "Up Next:\n (none)" if up_next.empty?
177
+
178
+ lines = up_next.map { |task| format_status_line(task) }
179
+ "Up Next:\n#{lines.join("\n")}"
180
+ end
181
+
182
+ # Format the "Recently Done" section.
183
+ def self.format_recently_done_section(recently_done)
184
+ return "Recently Done:\n (none)" if recently_done.empty?
185
+
186
+ lines = recently_done.map do |entry|
187
+ task = entry[:item]
188
+ time_str = Ace::Support::Items::Atoms::RelativeTimeFormatter.format(entry[:completed_at])
189
+ " #{format_status_line(task).strip} (#{time_str})"
190
+ end
191
+ "Recently Done:\n#{lines.join("\n")}"
192
+ end
193
+
194
+ private_class_method :format_up_next_section, :format_recently_done_section
195
+
196
+ # Format a single task as a compact list item.
197
+ def self.format_list_item(task)
198
+ c = Ace::Support::Items::Atoms::AnsiColors
199
+ status_sym = colored_status_sym(task.status)
200
+ priority_sym = PRIORITY_LABELS[task.priority] || ""
201
+ priority_color = PRIORITY_COLORS[task.priority]
202
+ priority_prefix = if priority_sym.empty?
203
+ " "
204
+ elsif priority_color
205
+ "#{c.colorize(priority_sym, priority_color)} "
206
+ else
207
+ "#{priority_sym} "
208
+ end
209
+ id_str = c.colorize(task.id, c::DIM)
210
+ subtask_str = task.has_subtasks? ? c.colorize(" \u203a#{task.subtasks.length}", c::DIM) : ""
211
+ tags_str = (task.tags && task.tags.any?) ? c.colorize(" [#{task.tags.join(", ")}]", c::DIM) : ""
212
+ folder_str = task.special_folder ? c.colorize(" (#{task.special_folder})", c::DIM) : ""
213
+
214
+ "#{status_sym} #{priority_prefix}#{id_str} #{task.title}#{subtask_str}#{tags_str}#{folder_str}"
215
+ end
216
+
217
+ private_class_method :format_list_item
218
+ end
219
+ end
220
+ end
221
+ end