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,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Task
5
+ module Atoms
6
+ # Builds default frontmatter hash for new tasks matching the task schema.
7
+ module TaskFrontmatterDefaults
8
+ VALID_STATUSES = %w[pending in-progress done blocked draft skipped cancelled].freeze
9
+ VALID_PRIORITIES = %w[critical high medium low].freeze
10
+
11
+ # Build a frontmatter hash with defaults for missing values.
12
+ #
13
+ # @param id [String] Formatted task ID (required)
14
+ # @param status [String] Task status (default: "pending")
15
+ # @param priority [String, nil] Priority level
16
+ # @param tags [Array<String>] Tags
17
+ # @param dependencies [Array<String>] Dependency task IDs
18
+ # @param created_at [Time, nil] Creation time
19
+ # @param parent [String, nil] Parent task ID for subtasks
20
+ # @return [Hash] Frontmatter hash
21
+ def self.build(id:, status: "pending", priority: nil, tags: [], dependencies: [], created_at: nil, parent: nil, estimate: nil)
22
+ fm = {
23
+ "id" => id,
24
+ "status" => status || "pending",
25
+ "priority" => priority || "medium",
26
+ "created_at" => format_time(created_at)
27
+ }
28
+ fm["estimate"] = estimate
29
+ fm["dependencies"] = dependencies || []
30
+ fm["tags"] = tags || []
31
+ fm["parent"] = parent if parent
32
+ fm
33
+ end
34
+
35
+ # Format time for frontmatter
36
+ # @param time [Time, nil]
37
+ # @return [String, nil]
38
+ def self.format_time(time)
39
+ return nil unless time
40
+
41
+ time.strftime("%Y-%m-%d %H:%M:%S")
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/b36ts"
4
+ require "ace/support/items"
5
+
6
+ module Ace
7
+ module Task
8
+ module Atoms
9
+ # Wraps ItemIdFormatter with the ".t." type marker for tasks.
10
+ #
11
+ # @example Generate and format a task ID
12
+ # TaskIdFormatter.generate
13
+ # # => ItemId(prefix: "8pp", type_marker: "t", suffix: "q7w", formatted_id: "8pp.t.q7w")
14
+ #
15
+ # @example Format an existing b36ts ID
16
+ # TaskIdFormatter.format("8ppq7w")
17
+ # # => ItemId with formatted_id "8pp.t.q7w"
18
+ class TaskIdFormatter
19
+ TYPE_MARKER = "t"
20
+
21
+ # Generate a new task ID from current time
22
+ # @param time [Time] Time to encode (default: now)
23
+ # @return [Ace::Support::Items::Models::ItemId]
24
+ def self.generate(time = Time.now.utc)
25
+ raw = Ace::B36ts.encode(time, format: :"2sec")
26
+ Ace::Support::Items::Atoms::ItemIdFormatter.split(raw, type_marker: TYPE_MARKER)
27
+ end
28
+
29
+ # Format an existing 6-char b36ts ID as a task ID
30
+ # @param raw_b36ts [String] 6-character b36ts ID
31
+ # @return [Ace::Support::Items::Models::ItemId]
32
+ def self.format(raw_b36ts)
33
+ Ace::Support::Items::Atoms::ItemIdFormatter.split(raw_b36ts, type_marker: TYPE_MARKER)
34
+ end
35
+
36
+ # Reconstruct raw b36ts from a formatted task ID
37
+ # @param formatted_id [String] e.g., "8pp.t.q7w"
38
+ # @return [String] Raw 6-char b36ts ID
39
+ def self.reconstruct(formatted_id)
40
+ Ace::Support::Items::Atoms::ItemIdFormatter.reconstruct(formatted_id)
41
+ end
42
+
43
+ # Build folder name from formatted ID and slug
44
+ # @param formatted_id [String] e.g., "8pp.t.q7w"
45
+ # @param slug [String] e.g., "fix-login"
46
+ # @return [String] e.g., "8pp.t.q7w-fix-login"
47
+ def self.folder_name(formatted_id, slug)
48
+ Ace::Support::Items::Atoms::ItemIdFormatter.folder_name(formatted_id, slug)
49
+ end
50
+
51
+ # Build spec filename from formatted ID and slug
52
+ # @param formatted_id [String] e.g., "8pp.t.q7w"
53
+ # @param slug [String] e.g., "fix-login"
54
+ # @return [String] e.g., "8pp.t.q7w-fix-login.s.md"
55
+ def self.spec_filename(formatted_id, slug)
56
+ base = folder_name(formatted_id, slug)
57
+ "#{base}.s.md"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Task
5
+ module Atoms
6
+ module TaskValidationRules
7
+ VALID_STATUSES = %w[pending in-progress done blocked draft skipped cancelled].freeze
8
+ TERMINAL_STATUSES = %w[done skipped cancelled].freeze
9
+ REQUIRED_FIELDS = %w[id status title].freeze
10
+ RECOMMENDED_FIELDS = %w[tags created_at].freeze
11
+ MAX_TITLE_LENGTH = 80
12
+
13
+ def self.valid_status?(status)
14
+ VALID_STATUSES.include?(status.to_s)
15
+ end
16
+
17
+ def self.terminal_status?(status)
18
+ TERMINAL_STATUSES.include?(status.to_s)
19
+ end
20
+
21
+ def self.valid_id?(id)
22
+ id.to_s.match?(/^[0-9a-z]{3}\.[a-z]\.[0-9a-z]{3}$/)
23
+ end
24
+
25
+ def self.scope_consistent?(status, special_folder)
26
+ issues = []
27
+ if terminal_status?(status) && special_folder != "_archive"
28
+ issues << {type: :warning, message: "Task with terminal status '#{status}' not in _archive/"}
29
+ end
30
+ if special_folder == "_archive" && !terminal_status?(status) && status
31
+ issues << {type: :warning, message: "Task in _archive/ but status is '#{status}' (expected terminal status)"}
32
+ end
33
+ if special_folder == "_maybe" && terminal_status?(status)
34
+ issues << {type: :warning, message: "Task in _maybe/ with terminal status '#{status}' (should be in _archive/)"}
35
+ end
36
+ issues
37
+ end
38
+
39
+ def self.missing_required_fields(frontmatter)
40
+ return REQUIRED_FIELDS.dup if frontmatter.nil? || !frontmatter.is_a?(Hash)
41
+ REQUIRED_FIELDS.select { |field| frontmatter[field].nil? || frontmatter[field].to_s.strip.empty? }
42
+ end
43
+
44
+ def self.missing_recommended_fields(frontmatter)
45
+ return RECOMMENDED_FIELDS.dup if frontmatter.nil? || !frontmatter.is_a?(Hash)
46
+ RECOMMENDED_FIELDS.select { |field| frontmatter[field].nil? }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+
5
+ module Ace
6
+ module Task
7
+ module CLI
8
+ module Commands
9
+ # ace-support-cli Command class for ace-task create
10
+ class Create < Ace::Support::Cli::Command
11
+ include Ace::Support::Cli::Base
12
+
13
+ desc <<~DESC.strip
14
+ Create a new task
15
+
16
+ Creates a new task with a B36TS-based type-marked ID (xxx.t.yyy).
17
+ DESC
18
+
19
+ example [
20
+ '"Fix login bug" # Create task with title',
21
+ '"Fix auth" --priority high --tags auth,security # With priority and tags',
22
+ '"Setup DB" --child-of q7w # Create as subtask',
23
+ '"Quick task" --in maybe # Create in _maybe/ folder',
24
+ '"Draft spec" --status draft --estimate TBD # Create as draft with estimate',
25
+ '"Preview only" --dry-run # Show what would be created'
26
+ ]
27
+
28
+ argument :title, required: true, desc: "Task title"
29
+
30
+ option :priority, type: :string, aliases: %w[-p], desc: "Priority (critical, high, medium, low)"
31
+ option :tags, type: :string, aliases: %w[-T], desc: "Tags (comma-separated)"
32
+ option :status, type: :string, aliases: %w[-s], desc: "Initial status (draft, pending, blocked, ...)"
33
+ option :estimate, type: :string, aliases: %w[-e], desc: "Effort estimate (e.g. TBD, 2h, 1d)"
34
+ option :"child-of", type: :string, desc: "Parent task reference (creates subtask)"
35
+ option :in, type: :string, aliases: %w[-i], desc: "Target folder (e.g. next, maybe)"
36
+ option :"dry-run", type: :boolean, aliases: %w[-n], desc: "Preview without writing"
37
+
38
+ option :git_commit, type: :boolean, aliases: %w[--gc], desc: "Auto-commit changes"
39
+
40
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
41
+ option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
42
+ option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
43
+
44
+ def call(title:, **options)
45
+ dry_run = options[:"dry-run"]
46
+ priority = options[:priority]
47
+ tags_str = options[:tags]
48
+ tags = tags_str ? tags_str.split(",").map(&:strip).reject(&:empty?) : []
49
+ status = options[:status]
50
+ estimate = options[:estimate]
51
+ child_of = options[:"child-of"]
52
+ in_folder = options[:in]
53
+
54
+ if status
55
+ valid = Ace::Task::Atoms::TaskValidationRules::VALID_STATUSES
56
+ unless valid.include?(status)
57
+ raise Ace::Support::Cli::Error.new("Invalid status '#{status}'. Valid: #{valid.join(", ")}")
58
+ end
59
+ end
60
+
61
+ if dry_run
62
+ puts "Would create task:"
63
+ puts " Title: #{title}"
64
+ puts " Status: #{status}" if status
65
+ puts " Priority: #{priority}" if priority
66
+ puts " Estimate: #{estimate}" if estimate
67
+ puts " Tags: #{tags.join(", ")}" if tags.any?
68
+ puts " Parent: #{child_of}" if child_of
69
+ puts " Folder: #{in_folder}" if in_folder
70
+ return
71
+ end
72
+
73
+ manager = Ace::Task::Organisms::TaskManager.new
74
+
75
+ task = if child_of
76
+ manager.create_subtask(child_of, title, status: status, priority: priority, tags: tags, estimate: estimate)
77
+ else
78
+ manager.create(title, status: status, priority: priority, tags: tags, estimate: estimate)
79
+ end
80
+
81
+ unless task
82
+ raise Ace::Support::Cli::Error.new("Parent task '#{child_of}' not found") if child_of
83
+ raise Ace::Support::Cli::Error.new("Failed to create task")
84
+ end
85
+
86
+ # Move to folder if specified
87
+ if in_folder && !child_of
88
+ task = manager.move(task.id, to: in_folder)
89
+ end
90
+
91
+ puts "Created task #{task.id}"
92
+ puts " Path: #{task.file_path}"
93
+
94
+ if options[:git_commit]
95
+ Ace::Support::Items::Molecules::GitCommitter.commit(
96
+ paths: [task.path],
97
+ intention: "create task #{task.id}"
98
+ )
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require_relative "../../organisms/task_doctor"
6
+ require_relative "../../molecules/task_doctor_fixer"
7
+ require_relative "../../molecules/task_doctor_reporter"
8
+ require_relative "../../molecules/task_config_loader"
9
+
10
+ module Ace
11
+ module Task
12
+ module CLI
13
+ module Commands
14
+ # ace-support-cli Command class for ace-task doctor
15
+ #
16
+ # Runs health checks on tasks and optionally auto-fixes issues.
17
+ class Doctor < Ace::Support::Cli::Command
18
+ include Ace::Support::Cli::Base
19
+
20
+ desc <<~DESC.strip
21
+ Run health checks on tasks
22
+
23
+ Validates frontmatter, file structure, and scope/status consistency
24
+ across all tasks in the repository. Supports auto-fixing safe issues.
25
+
26
+ DESC
27
+
28
+ example [
29
+ " # Run all health checks",
30
+ "--auto-fix # Auto-fix safe issues",
31
+ "--auto-fix --dry-run # Preview fixes without applying",
32
+ "--auto-fix-with-agent # Auto-fix then launch agent for remaining",
33
+ "--check frontmatter # Run specific check (frontmatter|structure|scope)",
34
+ "--json # Output as JSON",
35
+ "--verbose # Show all warnings"
36
+ ]
37
+
38
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
39
+ option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
40
+ option :auto_fix, type: :boolean, aliases: %w[-f], desc: "Auto-fix safe issues"
41
+ option :auto_fix_with_agent, type: :boolean, desc: "Auto-fix then launch agent for remaining"
42
+ option :model, type: :string, desc: "Provider:model for agent session"
43
+ option :errors_only, type: :boolean, desc: "Show only errors, not warnings"
44
+ option :no_color, type: :boolean, desc: "Disable colored output"
45
+ option :json, type: :boolean, desc: "Output in JSON format"
46
+ option :dry_run, type: :boolean, aliases: %w[-n], desc: "Preview fixes without applying"
47
+ option :check, type: :string, desc: "Run specific check (frontmatter, structure, scope)"
48
+
49
+ def call(**options)
50
+ execute_doctor(options)
51
+ end
52
+
53
+ private
54
+
55
+ def execute_doctor(options)
56
+ config = Molecules::TaskConfigLoader.load
57
+ root_dir = Molecules::TaskConfigLoader.root_dir(config)
58
+
59
+ unless Dir.exist?(root_dir)
60
+ puts "Error: Tasks directory not found: #{root_dir}"
61
+ raise Ace::Support::Cli::Error.new("Tasks directory not found")
62
+ end
63
+
64
+ # Normalize options
65
+ format = options[:json] ? :json : :terminal
66
+ fix = options[:auto_fix] || options[:auto_fix_with_agent]
67
+ colors = !options[:no_color]
68
+ colors = false if format == :json
69
+
70
+ doctor_opts = {}
71
+ doctor_opts[:check] = options[:check] if options[:check]
72
+
73
+ if options[:quiet]
74
+ results = run_diagnosis(root_dir, doctor_opts)
75
+ raise Ace::Support::Cli::Error.new("Health check failed") unless results[:valid]
76
+ return
77
+ end
78
+
79
+ results = run_diagnosis(root_dir, doctor_opts)
80
+
81
+ # Filter errors-only
82
+ if options[:errors_only] && results[:issues]
83
+ results[:issues] = results[:issues].select { |i| i[:type] == :error }
84
+ end
85
+
86
+ output = Molecules::TaskDoctorReporter.format_results(
87
+ results,
88
+ format: format,
89
+ verbose: options[:verbose],
90
+ colors: colors
91
+ )
92
+ puts output
93
+
94
+ if fix && results[:issues]&.any?
95
+ handle_auto_fix(results, root_dir, doctor_opts, options, colors)
96
+ end
97
+
98
+ if options[:auto_fix_with_agent]
99
+ handle_agent_fix(root_dir, doctor_opts, options, config)
100
+ end
101
+
102
+ raise Ace::Support::Cli::Error.new("Health check failed") unless results[:valid]
103
+ rescue Ace::Support::Cli::Error
104
+ raise
105
+ rescue => e
106
+ raise Ace::Support::Cli::Error.new(e.message)
107
+ end
108
+
109
+ def run_diagnosis(root_dir, doctor_opts)
110
+ doctor = Organisms::TaskDoctor.new(root_dir, doctor_opts)
111
+ doctor.run_diagnosis
112
+ end
113
+
114
+ def handle_auto_fix(results, root_dir, doctor_opts, options, colors)
115
+ doctor = Organisms::TaskDoctor.new(root_dir, doctor_opts)
116
+ fixable_issues = results[:issues].select { |issue| doctor.auto_fixable?(issue) }
117
+
118
+ if fixable_issues.empty?
119
+ puts "\nNo auto-fixable issues found"
120
+ return
121
+ end
122
+
123
+ unless options[:quiet] || options[:dry_run]
124
+ puts "\nFound #{fixable_issues.size} auto-fixable issues"
125
+ print "Apply fixes? (y/N): "
126
+ response = $stdin.gets.chomp.downcase
127
+ return unless response == "y" || response == "yes"
128
+ end
129
+
130
+ fixer = Molecules::TaskDoctorFixer.new(dry_run: options[:dry_run], root_dir: root_dir)
131
+ fix_results = fixer.fix_issues(fixable_issues)
132
+
133
+ output = Molecules::TaskDoctorReporter.format_fix_results(
134
+ fix_results,
135
+ colors: colors
136
+ )
137
+ puts output
138
+
139
+ unless options[:dry_run]
140
+ puts "\nRe-running health check after fixes..."
141
+ new_results = run_diagnosis(root_dir, doctor_opts)
142
+
143
+ output = Molecules::TaskDoctorReporter.format_results(
144
+ new_results,
145
+ format: :summary,
146
+ verbose: false,
147
+ colors: colors
148
+ )
149
+ puts output
150
+ end
151
+ end
152
+
153
+ def handle_agent_fix(root_dir, doctor_opts, options, config)
154
+ require "ace/llm"
155
+ results = run_diagnosis(root_dir, doctor_opts)
156
+ remaining = results[:issues]&.reject { |i| i[:type] == :info }
157
+
158
+ if remaining.nil? || remaining.empty?
159
+ puts "\nNo remaining issues for agent to fix."
160
+ return
161
+ end
162
+
163
+ issue_list = remaining.map { |i|
164
+ prefix = (i[:type] == :error) ? "ERROR" : "WARNING"
165
+ "- [#{prefix}] #{i[:message]}#{" (#{i[:location]})" if i[:location]}"
166
+ }.join("\n")
167
+
168
+ provider_model = options[:model] || config.dig("task", "doctor_agent_model") || "gemini:flash-latest@yolo"
169
+
170
+ prompt = <<~PROMPT
171
+ The following #{remaining.size} task issues could NOT be auto-fixed and need manual intervention:
172
+
173
+ #{issue_list}
174
+
175
+ ---
176
+
177
+ Fix each issue listed above in the .ace-tasks/ directory.
178
+
179
+ IMPORTANT RULES:
180
+ - For invalid ID format issues, inspect the folder name and fix the frontmatter ID to match
181
+ - For YAML syntax errors, read the file and fix the YAML
182
+ - For missing opening delimiter, add '---' at the start of the file
183
+ - Do NOT delete content files — prefer fixing in place
184
+ - For folder naming issues, rename the folder to match {id}-{slug} convention
185
+
186
+ ---
187
+
188
+ Run `ace-task doctor --verbose` to verify all issues are fixed.
189
+ PROMPT
190
+
191
+ puts "\nLaunching agent to fix #{remaining.size} remaining issues..."
192
+ query_options = {
193
+ system: nil,
194
+ timeout: 600,
195
+ fallback: false
196
+ }
197
+
198
+ response = Ace::LLM::QueryInterface.query(provider_model, prompt, **query_options)
199
+
200
+ puts response[:text]
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+
5
+ module Ace
6
+ module Task
7
+ module CLI
8
+ module Commands
9
+ # ace-support-cli Command class for ace-task list
10
+ class List < Ace::Support::Cli::Command
11
+ include Ace::Support::Cli::Base
12
+
13
+ C = Ace::Support::Items::Atoms::AnsiColors
14
+ desc "List tasks\n\n" \
15
+ "Lists all tasks with optional filtering by status, tags, or folder.\n\n" \
16
+ "Status legend:\n" \
17
+ " #{C::CYAN}◇ draft#{C::RESET} #{C::RESET}○ pending #{C::YELLOW}▶ in-progress#{C::RESET} #{C::GREEN}✓ done#{C::RESET}\n" \
18
+ " #{C::RED}✗ blocked#{C::RESET} #{C::DIM}– skipped — cancelled#{C::RESET}\n\n" \
19
+ "Priority: #{C::RED}▲ critical#{C::RESET} ▲ high #{C::DIM}▼ low#{C::RESET} Subtasks: ›N"
20
+ remove_const(:C)
21
+
22
+ example [
23
+ " # Active tasks (root only, default)",
24
+ "--in all # All tasks including archived/maybe",
25
+ "--in maybe # Tasks in _maybe/",
26
+ "--status pending # Filter by status",
27
+ "--tags ux,design # Tasks matching any tag",
28
+ "--in next --status pending # Combined filters",
29
+ "--filter status:pending --filter tags:ux|design # Generic filters",
30
+ "--sort id # Sort by ID (chronological)",
31
+ "--sort priority # Sort by priority level"
32
+ ]
33
+
34
+ option :status, type: :string, aliases: %w[-s], desc: "Filter by status (pending, in-progress, done, blocked)"
35
+ option :tags, type: :string, aliases: %w[-T], desc: "Filter by tags (comma-separated, any match)"
36
+ option :in, type: :string, aliases: %w[-i], desc: "Filter by folder (next=root only [default], all=everything, maybe, archive)"
37
+ option :root, type: :string, aliases: %w[-r], desc: "Override root path (subpath within tasks root)"
38
+ option :filter, type: :array, aliases: %w[-f], desc: "Filter by key:value (repeatable, supports key:a|b and key:!value)"
39
+ option :sort, type: :string, aliases: %w[-S], desc: "Sort order: smart (default), id, priority, created"
40
+
41
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
42
+ option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
43
+ option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
44
+
45
+ def call(**options)
46
+ status = options[:status]
47
+ in_folder = options[:in]
48
+ root = options[:root]
49
+ tags_str = options[:tags]
50
+ tags = tags_str ? tags_str.split(",").map(&:strip).reject(&:empty?) : []
51
+ filters = options[:filter]
52
+ sort = options[:sort] || "smart"
53
+
54
+ manager = if root
55
+ Ace::Task::Organisms::TaskManager.new(root_dir: File.expand_path(root))
56
+ else
57
+ Ace::Task::Organisms::TaskManager.new
58
+ end
59
+
60
+ list_opts = {status: status, tags: tags, filters: filters, sort: sort}
61
+ list_opts[:in_folder] = in_folder if in_folder
62
+ tasks = manager.list(**list_opts)
63
+
64
+ puts Ace::Task::Molecules::TaskDisplayFormatter.format_list(
65
+ tasks, total_count: manager.last_list_total,
66
+ global_folder_stats: manager.last_folder_counts
67
+ )
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require_relative "../../molecules/task_plan_cache"
5
+ require_relative "../../molecules/task_plan_generator"
6
+ require_relative "../../molecules/task_config_loader"
7
+ require_relative "../../organisms/task_manager"
8
+
9
+ module Ace
10
+ module Task
11
+ module CLI
12
+ module Commands
13
+ # ace-support-cli Command class for ace-task plan
14
+ class Plan < Ace::Support::Cli::Command
15
+ include Ace::Support::Cli::Base
16
+
17
+ desc <<~DESC.strip
18
+ Resolve or generate a task implementation plan
19
+
20
+ Reuses fresh cached plans when available, otherwise regenerates.
21
+ DESC
22
+
23
+ example [
24
+ "q7w # Reuse fresh plan or generate new",
25
+ "q7w --refresh # Force regeneration",
26
+ "q7w --content # Print full plan content",
27
+ "q7w --model gemini:flash-latest # Override planning model"
28
+ ]
29
+
30
+ argument :ref, required: true, desc: "Task reference (full ID, short ref, or suffix)"
31
+
32
+ option :refresh, type: :boolean, desc: "Force plan regeneration"
33
+ option :content, type: :boolean, desc: "Print full plan content instead of path"
34
+ option :model, type: :string, desc: "Provider:model override for plan generation"
35
+
36
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
37
+ option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
38
+ option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
39
+
40
+ class << self
41
+ attr_accessor :generator_class
42
+ end
43
+
44
+ def call(ref:, **options)
45
+ task = resolve_task(ref)
46
+ cache = Molecules::TaskPlanCache.new(task_id: task.id)
47
+ config = Molecules::TaskConfigLoader.load
48
+
49
+ plan_path = cache.resolve_latest_plan
50
+ refresh = options[:refresh]
51
+
52
+ unless refresh
53
+ if plan_path && cache.fresh?(plan_path, task_file: task.file_path)
54
+ output_plan(plan_path, options)
55
+ return
56
+ end
57
+ end
58
+
59
+ context_files = capture_context_files(task)
60
+ model = options[:model] || default_model(config)
61
+ generator = plan_generator(model)
62
+ content = generator.generate(
63
+ task: task,
64
+ context_files: context_files,
65
+ cache_dir: cache.cache_dir
66
+ )
67
+ plan_path = cache.write_plan(
68
+ content: content,
69
+ model: model,
70
+ task_file: task.file_path,
71
+ context_files: context_files,
72
+ prompt_files: generator.prompt_paths
73
+ )
74
+
75
+ output_plan(plan_path, options)
76
+ end
77
+
78
+ private
79
+
80
+ def resolve_task(ref)
81
+ manager = Organisms::TaskManager.new
82
+ task = manager.show(ref)
83
+ return task if task
84
+
85
+ raise Ace::Support::Cli::Error.new("Task '#{ref}' not found. Run `ace-task list` to discover valid refs.")
86
+ end
87
+
88
+ def capture_context_files(task)
89
+ files = Array(task.metadata.dig("bundle", "files"))
90
+ expanded = files.map { |path| File.expand_path(path, Dir.pwd) }.uniq
91
+ resolved = expanded.select { |path| File.file?(path) }
92
+
93
+ missing = expanded - resolved
94
+ missing.each { |path| warn "Warning: context file not found: #{path}" } if missing.any?
95
+
96
+ resolved
97
+ end
98
+
99
+ def output_plan(plan_path, options)
100
+ if options[:content]
101
+ puts File.read(plan_path)
102
+ else
103
+ puts plan_path
104
+ end
105
+ end
106
+
107
+ def plan_generator(model, cli_args: nil)
108
+ klass = self.class.generator_class || Molecules::TaskPlanGenerator
109
+ klass.new(model: model, cli_args: cli_args)
110
+ end
111
+
112
+ def default_model(config)
113
+ config.dig("task", "plan", "model") || "gemini:flash-latest"
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end