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,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
|