ace-task 0.39.2 → 0.40.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 +4 -4
- data/CHANGELOG.md +13 -0
- data/lib/ace/task/cli/commands/create.rb +128 -4
- data/lib/ace/task/cli/commands/show.rb +7 -1
- data/lib/ace/task/cli/commands/update.rb +7 -1
- data/lib/ace/task/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7e07d26b4b14644e4227398532fa33c32066628db686f51274628d8164608225
|
|
4
|
+
data.tar.gz: ae7def12a1bacbcd29744736de409970b612e943a48e291c8bdf6fdbb6480424
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49a85005d9283e2920f421957bc4d5e2f6b5a181c8259a3d8e63c5206a2fdcde6c4ea4342be3cbb84acce05fe12895f3a2e57301202c362b8a1a305ddd1e141b
|
|
7
|
+
data.tar.gz: dddf45e0bd3c18cef1cf1568987f91ab56b3f15567dee849ced55483e818fd147c4e0dfaadc95a872add0ab6510c4ded2538bd7a07051d04ec1306c44e6c48bb
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.40.0] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Support exact --root selection in create, show and update so project objective queues can use the configured directory without changing the global _folder convention.
|
|
15
|
+
|
|
16
|
+
## [0.39.3] - 2026-10-12
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Route `ace-task create --in FOLDER` through the task manager's relocation API
|
|
21
|
+
and remove the newly created task artifacts when destination placement fails.
|
|
22
|
+
|
|
10
23
|
## [0.39.2] - 2026-10-06
|
|
11
24
|
|
|
12
25
|
### Fixed
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "ace/support/cli"
|
|
4
|
+
require "fileutils"
|
|
4
5
|
|
|
5
6
|
module Ace
|
|
6
7
|
module Task
|
|
@@ -10,6 +11,9 @@ module Ace
|
|
|
10
11
|
class Create < Ace::Support::Cli::Command
|
|
11
12
|
include Ace::Support::Cli::Base
|
|
12
13
|
|
|
14
|
+
# "next" is a move-to-root alias and is validated separately.
|
|
15
|
+
BUILT_IN_DESTINATIONS = %w[maybe anytime archive].freeze
|
|
16
|
+
|
|
13
17
|
desc <<~DESC.strip
|
|
14
18
|
Create a new task
|
|
15
19
|
|
|
@@ -38,11 +42,14 @@ module Ace
|
|
|
38
42
|
option :server, type: :string, desc: "Named forge server"
|
|
39
43
|
option :"default-server", type: :boolean, desc: "Use the configured default forge server"
|
|
40
44
|
option :"child-of", type: :string, desc: "Parent task reference (creates subtask)"
|
|
41
|
-
option :in, type: :string, aliases: %w[-i],
|
|
45
|
+
option :in, type: :string, aliases: %w[-i],
|
|
46
|
+
desc: "Top-level target (next, maybe, anytime, archive, or existing custom folder)"
|
|
42
47
|
option :"dry-run", type: :boolean, aliases: %w[-n], desc: "Preview without writing"
|
|
43
48
|
|
|
44
49
|
option :git_commit, type: :boolean, aliases: %w[--gc], desc: "Auto-commit changes"
|
|
45
50
|
|
|
51
|
+
option :root, type: :string, aliases: %w[-r], desc: "Exact task root directory (overrides project config)"
|
|
52
|
+
|
|
46
53
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
47
54
|
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
48
55
|
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
@@ -72,6 +79,14 @@ module Ace
|
|
|
72
79
|
end
|
|
73
80
|
end
|
|
74
81
|
|
|
82
|
+
manager = if options[:root]
|
|
83
|
+
Ace::Task::Organisms::TaskManager.new(root_dir: File.expand_path(options[:root]))
|
|
84
|
+
else
|
|
85
|
+
Ace::Task::Organisms::TaskManager.new
|
|
86
|
+
end
|
|
87
|
+
validate_destination!(manager, in_folder) if in_folder && !child_of
|
|
88
|
+
placement_directories_before = directories_under(manager.root_dir) if in_folder && !child_of
|
|
89
|
+
|
|
75
90
|
if dry_run
|
|
76
91
|
puts "Would create task:"
|
|
77
92
|
puts " Title: #{title}"
|
|
@@ -85,8 +100,6 @@ module Ace
|
|
|
85
100
|
return
|
|
86
101
|
end
|
|
87
102
|
|
|
88
|
-
manager = Ace::Task::Organisms::TaskManager.new
|
|
89
|
-
|
|
90
103
|
task = begin
|
|
91
104
|
if child_of
|
|
92
105
|
manager.create_subtask(
|
|
@@ -119,7 +132,7 @@ module Ace
|
|
|
119
132
|
|
|
120
133
|
# Move to folder if specified
|
|
121
134
|
if in_folder && !child_of
|
|
122
|
-
task = manager
|
|
135
|
+
task = place_created_task(manager, task, in_folder, placement_directories_before)
|
|
123
136
|
end
|
|
124
137
|
|
|
125
138
|
puts "Created task #{task.id}"
|
|
@@ -140,6 +153,117 @@ module Ace
|
|
|
140
153
|
|
|
141
154
|
private
|
|
142
155
|
|
|
156
|
+
def validate_destination!(manager, target_folder)
|
|
157
|
+
detector = Ace::Support::Items::Atoms::SpecialFolderDetector
|
|
158
|
+
return if detector.move_to_root?(target_folder)
|
|
159
|
+
|
|
160
|
+
if detector.virtual_filter?(target_folder)
|
|
161
|
+
raise Ace::Support::Cli::Error,
|
|
162
|
+
"Destination '#{target_folder}' is a virtual list filter, not a physical folder."
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
normalized = detector.normalize(target_folder)
|
|
166
|
+
configured_path = File.join(manager.root_dir, normalized)
|
|
167
|
+
normalized_built_ins = BUILT_IN_DESTINATIONS.map { |name| detector.normalize(name) }
|
|
168
|
+
valid = !target_folder.strip.empty? &&
|
|
169
|
+
!normalized.include?(File::SEPARATOR) &&
|
|
170
|
+
!normalized.include?("..") &&
|
|
171
|
+
(normalized_built_ins.include?(normalized) || Dir.exist?(configured_path)) &&
|
|
172
|
+
destination_within_root?(manager.root_dir, configured_path)
|
|
173
|
+
return if valid
|
|
174
|
+
|
|
175
|
+
choices = (["next"] + BUILT_IN_DESTINATIONS).join(", ")
|
|
176
|
+
raise Ace::Support::Cli::Error,
|
|
177
|
+
"Destination '#{target_folder}' does not exist. " \
|
|
178
|
+
"Use #{choices}, or an existing custom task folder."
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def destination_within_root?(root, destination)
|
|
182
|
+
return true unless File.exist?(destination)
|
|
183
|
+
|
|
184
|
+
root_path = File.realpath(root)
|
|
185
|
+
destination_path = File.realpath(destination)
|
|
186
|
+
destination_path.start_with?(root_path + File::SEPARATOR)
|
|
187
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
188
|
+
false
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def place_created_task(manager, task, target_folder, directories_before)
|
|
192
|
+
placed_task = manager.update(task.id, move_to: target_folder)
|
|
193
|
+
expected_folder = if Ace::Support::Items::Atoms::SpecialFolderDetector.move_to_root?(target_folder)
|
|
194
|
+
nil
|
|
195
|
+
else
|
|
196
|
+
Ace::Support::Items::Atoms::SpecialFolderDetector.normalize(target_folder)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
unless placed_task && placed_task.special_folder == expected_folder
|
|
200
|
+
actual_folder = if placed_task
|
|
201
|
+
placed_task.special_folder || "root"
|
|
202
|
+
else
|
|
203
|
+
"no task returned"
|
|
204
|
+
end
|
|
205
|
+
# This internal signal is wrapped below with rollback and an actionable CLI error.
|
|
206
|
+
raise "placement returned #{actual_folder.inspect}, expected #{expected_folder || "root"}"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
placed_task
|
|
210
|
+
rescue Ace::Git::Error
|
|
211
|
+
# update retains linked tasks after post-placement sync failures as
|
|
212
|
+
# recovery records; those are not destination-placement failures.
|
|
213
|
+
raise
|
|
214
|
+
rescue StandardError => e
|
|
215
|
+
begin
|
|
216
|
+
remove_created_task_artifacts(manager, task)
|
|
217
|
+
remove_empty_directories_created_since(manager.root_dir, directories_before)
|
|
218
|
+
rescue StandardError => cleanup_error
|
|
219
|
+
raise Ace::Support::Cli::Error,
|
|
220
|
+
"Failed to create task in '#{target_folder}': #{e.message}. " \
|
|
221
|
+
"Cleanup also failed: #{cleanup_error.message}"
|
|
222
|
+
end
|
|
223
|
+
raise Ace::Support::Cli::Error,
|
|
224
|
+
"Failed to create task in '#{target_folder}': #{e.message}. " \
|
|
225
|
+
"No task was created; fix the destination and retry."
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def remove_created_task_artifacts(manager, task)
|
|
229
|
+
root = File.expand_path(manager.root_dir)
|
|
230
|
+
candidates = [task.path]
|
|
231
|
+
candidates.concat(
|
|
232
|
+
Dir.glob(File.join(root, "**", "#{task.id}-*")).select { |path| File.directory?(path) }
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
candidates.uniq.each do |path|
|
|
236
|
+
expanded = File.expand_path(path)
|
|
237
|
+
next unless expanded.start_with?(root + File::SEPARATOR)
|
|
238
|
+
|
|
239
|
+
FileUtils.rm_rf(expanded)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
remaining = Dir.glob(File.join(root, "**", "#{task.id}-*")).select { |path| File.exist?(path) }
|
|
243
|
+
return if remaining.empty?
|
|
244
|
+
|
|
245
|
+
raise Ace::Support::Cli::Error,
|
|
246
|
+
"Failed to remove partial task artifacts after placement failure: #{remaining.join(", ")}"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def directories_under(root)
|
|
250
|
+
return [] unless Dir.exist?(root)
|
|
251
|
+
|
|
252
|
+
[File.expand_path(root)] + Dir.glob(File.join(root, "**", "*"))
|
|
253
|
+
.select { |path| File.directory?(path) }
|
|
254
|
+
.map { |path| File.expand_path(path) }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def remove_empty_directories_created_since(root, directories_before)
|
|
258
|
+
# Only directories created during this short command transaction
|
|
259
|
+
# are candidates, and non-empty concurrent directories are retained.
|
|
260
|
+
(directories_under(root) - directories_before).sort_by(&:length).reverse_each do |path|
|
|
261
|
+
Dir.rmdir(path)
|
|
262
|
+
rescue Errno::ENOENT, Errno::ENOTEMPTY
|
|
263
|
+
next
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
143
267
|
def resolve_title(positional_title, flag_title)
|
|
144
268
|
positional_present = present_string?(positional_title)
|
|
145
269
|
flag_present = present_string?(flag_title)
|
|
@@ -31,12 +31,18 @@ module Ace
|
|
|
31
31
|
option :content, type: :boolean, desc: "Print raw markdown content"
|
|
32
32
|
option :tree, type: :boolean, desc: "Show parent + subtask tree view"
|
|
33
33
|
|
|
34
|
+
option :root, type: :string, aliases: %w[-r], desc: "Exact task root directory (overrides project config)"
|
|
35
|
+
|
|
34
36
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
35
37
|
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
36
38
|
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
37
39
|
|
|
38
40
|
def call(ref:, **options)
|
|
39
|
-
manager =
|
|
41
|
+
manager = if options[:root]
|
|
42
|
+
Ace::Task::Organisms::TaskManager.new(root_dir: File.expand_path(options[:root]))
|
|
43
|
+
else
|
|
44
|
+
Ace::Task::Organisms::TaskManager.new
|
|
45
|
+
end
|
|
40
46
|
task = manager.show(ref)
|
|
41
47
|
|
|
42
48
|
unless task
|
|
@@ -46,6 +46,8 @@ module Ace
|
|
|
46
46
|
|
|
47
47
|
option :git_commit, type: :boolean, aliases: %w[--gc], desc: "Auto-commit changes"
|
|
48
48
|
|
|
49
|
+
option :root, type: :string, aliases: %w[-r], desc: "Exact task root directory (overrides project config)"
|
|
50
|
+
|
|
49
51
|
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
50
52
|
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
51
53
|
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
@@ -75,7 +77,11 @@ module Ace
|
|
|
75
77
|
add_hash = parse_kv_pairs(add_args)
|
|
76
78
|
remove_hash = parse_kv_pairs(remove_args)
|
|
77
79
|
|
|
78
|
-
manager =
|
|
80
|
+
manager = if options[:root]
|
|
81
|
+
Ace::Task::Organisms::TaskManager.new(root_dir: File.expand_path(options[:root]))
|
|
82
|
+
else
|
|
83
|
+
Ace::Task::Organisms::TaskManager.new
|
|
84
|
+
end
|
|
79
85
|
|
|
80
86
|
# Resolve --position into a set or remove operation
|
|
81
87
|
if position_arg
|
data/lib/ace/task/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-task
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.40.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-core
|