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.
- checksums.yaml +7 -0
- data/.ace-defaults/nav/protocols/skill-sources/ace-task.yml +19 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-task.yml +19 -0
- data/.ace-defaults/task/config.yml +25 -0
- data/CHANGELOG.md +518 -0
- data/README.md +52 -0
- data/Rakefile +12 -0
- data/exe/ace-task +22 -0
- data/handbook/guides/task-definition.g.md +156 -0
- data/handbook/skills/as-bug-analyze/SKILL.md +26 -0
- data/handbook/skills/as-bug-fix/SKILL.md +27 -0
- data/handbook/skills/as-task-document-unplanned/SKILL.md +27 -0
- data/handbook/skills/as-task-draft/SKILL.md +24 -0
- data/handbook/skills/as-task-finder/SKILL.md +27 -0
- data/handbook/skills/as-task-plan/SKILL.md +30 -0
- data/handbook/skills/as-task-review/SKILL.md +25 -0
- data/handbook/skills/as-task-review-questions/SKILL.md +25 -0
- data/handbook/skills/as-task-update/SKILL.md +21 -0
- data/handbook/skills/as-task-work/SKILL.md +41 -0
- data/handbook/templates/task/draft.template.md +166 -0
- data/handbook/templates/task/file-modification-checklist.template.md +26 -0
- data/handbook/templates/task/technical-approach.template.md +26 -0
- data/handbook/workflow-instructions/bug/analyze.wf.md +458 -0
- data/handbook/workflow-instructions/bug/fix.wf.md +512 -0
- data/handbook/workflow-instructions/task/document-unplanned.wf.md +222 -0
- data/handbook/workflow-instructions/task/draft.wf.md +552 -0
- data/handbook/workflow-instructions/task/finder.wf.md +22 -0
- data/handbook/workflow-instructions/task/plan.wf.md +489 -0
- data/handbook/workflow-instructions/task/review-plan.wf.md +144 -0
- data/handbook/workflow-instructions/task/review-questions.wf.md +411 -0
- data/handbook/workflow-instructions/task/review-work.wf.md +146 -0
- data/handbook/workflow-instructions/task/review.wf.md +351 -0
- data/handbook/workflow-instructions/task/update.wf.md +118 -0
- data/handbook/workflow-instructions/task/work.wf.md +106 -0
- data/lib/ace/task/atoms/task_file_pattern.rb +68 -0
- data/lib/ace/task/atoms/task_frontmatter_defaults.rb +46 -0
- data/lib/ace/task/atoms/task_id_formatter.rb +62 -0
- data/lib/ace/task/atoms/task_validation_rules.rb +51 -0
- data/lib/ace/task/cli/commands/create.rb +105 -0
- data/lib/ace/task/cli/commands/doctor.rb +206 -0
- data/lib/ace/task/cli/commands/list.rb +73 -0
- data/lib/ace/task/cli/commands/plan.rb +119 -0
- data/lib/ace/task/cli/commands/show.rb +58 -0
- data/lib/ace/task/cli/commands/status.rb +77 -0
- data/lib/ace/task/cli/commands/update.rb +183 -0
- data/lib/ace/task/cli.rb +83 -0
- data/lib/ace/task/models/task.rb +46 -0
- data/lib/ace/task/molecules/path_utils.rb +20 -0
- data/lib/ace/task/molecules/subtask_creator.rb +130 -0
- data/lib/ace/task/molecules/task_config_loader.rb +92 -0
- data/lib/ace/task/molecules/task_creator.rb +115 -0
- data/lib/ace/task/molecules/task_display_formatter.rb +221 -0
- data/lib/ace/task/molecules/task_doctor_fixer.rb +510 -0
- data/lib/ace/task/molecules/task_doctor_reporter.rb +264 -0
- data/lib/ace/task/molecules/task_frontmatter_validator.rb +138 -0
- data/lib/ace/task/molecules/task_loader.rb +119 -0
- data/lib/ace/task/molecules/task_plan_cache.rb +190 -0
- data/lib/ace/task/molecules/task_plan_generator.rb +141 -0
- data/lib/ace/task/molecules/task_plan_prompt_builder.rb +91 -0
- data/lib/ace/task/molecules/task_reparenter.rb +247 -0
- data/lib/ace/task/molecules/task_resolver.rb +115 -0
- data/lib/ace/task/molecules/task_scanner.rb +129 -0
- data/lib/ace/task/molecules/task_structure_validator.rb +154 -0
- data/lib/ace/task/organisms/task_doctor.rb +199 -0
- data/lib/ace/task/organisms/task_manager.rb +353 -0
- data/lib/ace/task/version.rb +7 -0
- data/lib/ace/task.rb +37 -0
- 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
|