ace-task 0.38.1 → 0.39.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 +15 -0
- data/handbook/workflow-instructions/task/draft.wf.md +18 -18
- data/lib/ace/task/atoms/task_frontmatter_defaults.rb +5 -2
- data/lib/ace/task/cli/commands/create.rb +20 -13
- data/lib/ace/task/cli/commands/issue_link.rb +38 -0
- data/lib/ace/task/cli/commands/issue_sync.rb +42 -0
- data/lib/ace/task/cli/commands/update.rb +2 -0
- data/lib/ace/task/cli.rb +9 -5
- data/lib/ace/task/molecules/issue_link.rb +75 -0
- data/lib/ace/task/molecules/issue_sync_adapter.rb +99 -0
- data/lib/ace/task/molecules/subtask_creator.rb +2 -2
- data/lib/ace/task/molecules/task_creator.rb +2 -2
- data/lib/ace/task/molecules/task_frontmatter_validator.rb +44 -18
- data/lib/ace/task/molecules/task_plan_prompt_builder.rb +3 -3
- data/lib/ace/task/molecules/task_reparenter.rb +66 -0
- data/lib/ace/task/molecules/task_resolver.rb +31 -3
- data/lib/ace/task/organisms/task_manager.rb +780 -107
- data/lib/ace/task/version.rb +1 -1
- data/lib/ace/task.rb +0 -1
- metadata +24 -8
- data/lib/ace/task/cli/commands/github_sync.rb +0 -73
- data/lib/ace/task/molecules/github_issue_sync_adapter.rb +0 -88
|
@@ -76,9 +76,9 @@ module Ace
|
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
def run_ace_bundle(input, output_path)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
79
|
+
checkout_binstub = File.expand_path("../../../../../bin/ace-bundle", __dir__)
|
|
80
|
+
executable = File.file?(checkout_binstub) ? checkout_binstub : Gem.bin_path("ace-bundle", "ace-bundle")
|
|
81
|
+
_stdout, status = Open3.capture2(executable, input, "--format", "markdown-xml", "--output", output_path)
|
|
82
82
|
unless status.success?
|
|
83
83
|
raise Ace::Support::Cli::Error.new("ace-bundle failed for: #{input}")
|
|
84
84
|
end
|
|
@@ -75,6 +75,11 @@ module Ace
|
|
|
75
75
|
fm.delete("parent")
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Promoted descendants rebase from the standalone ID.
|
|
79
|
+
id_map = {task.id => new_id}
|
|
80
|
+
rewrite_descendants(new_dir, old_parent_id: task.id, new_parent_id: new_id, id_map: id_map)
|
|
81
|
+
rewrite_dependency_references(id_map)
|
|
82
|
+
|
|
78
83
|
loader = TaskLoader.new
|
|
79
84
|
loader.load(new_dir, id: new_id)
|
|
80
85
|
end
|
|
@@ -164,10 +169,71 @@ module Ace
|
|
|
164
169
|
fm["parent"] = parent_task.id
|
|
165
170
|
end
|
|
166
171
|
|
|
172
|
+
# Descendant identities hang off the parent ID: rewrite their
|
|
173
|
+
# frontmatter and spec filenames so persisted IDs stay authoritative.
|
|
174
|
+
id_map = {task.id => new_id}
|
|
175
|
+
rewrite_descendants(new_dir, old_parent_id: task.id, new_parent_id: new_id, id_map: id_map)
|
|
176
|
+
rewrite_dependency_references(id_map)
|
|
177
|
+
|
|
167
178
|
loader = TaskLoader.new
|
|
168
179
|
loader.load(new_dir, id: new_id)
|
|
169
180
|
end
|
|
170
181
|
|
|
182
|
+
# Rewrite dependency references in every task spec after an ID map
|
|
183
|
+
# changes (stale references would read as unmet dependencies).
|
|
184
|
+
def rewrite_dependency_references(id_map)
|
|
185
|
+
return if id_map.empty?
|
|
186
|
+
|
|
187
|
+
Dir.glob(File.join(@root_dir, "**", "*.s.md")).sort.each do |spec|
|
|
188
|
+
content = File.read(spec)
|
|
189
|
+
frontmatter, body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
|
|
190
|
+
deps = frontmatter["dependencies"]
|
|
191
|
+
next unless deps.is_a?(Array)
|
|
192
|
+
|
|
193
|
+
mapped = deps.map { |dep| id_map.fetch(dep, dep) }
|
|
194
|
+
next if mapped == deps
|
|
195
|
+
|
|
196
|
+
frontmatter["dependencies"] = mapped
|
|
197
|
+
tmp_path = "#{spec}.tmp.#{Process.pid}"
|
|
198
|
+
File.write(tmp_path, Ace::Support::Items::Atoms::FrontmatterSerializer.rebuild(frontmatter, body))
|
|
199
|
+
File.rename(tmp_path, spec)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Recursively rewrite descendant frontmatter IDs, parent references and
|
|
204
|
+
# spec filenames after their ancestor's ID changed.
|
|
205
|
+
def rewrite_descendants(dir, old_parent_id:, new_parent_id:, id_map: {})
|
|
206
|
+
return unless Dir.exist?(dir)
|
|
207
|
+
|
|
208
|
+
Dir.entries(dir).sort.each do |entry|
|
|
209
|
+
next if entry.start_with?(".")
|
|
210
|
+
next unless (short_match = entry.match(/\A([a-z0-9])-(.+)/))
|
|
211
|
+
|
|
212
|
+
char = short_match[1]
|
|
213
|
+
slug = short_match[2]
|
|
214
|
+
child_dir = File.join(dir, entry)
|
|
215
|
+
next unless File.directory?(child_dir)
|
|
216
|
+
|
|
217
|
+
old_child_id = "#{old_parent_id}.#{char}"
|
|
218
|
+
new_child_id = "#{new_parent_id}.#{char}"
|
|
219
|
+
id_map[old_child_id] = new_child_id
|
|
220
|
+
|
|
221
|
+
spec = Dir.glob(File.join(child_dir, "*.s.md")).sort.first
|
|
222
|
+
if spec
|
|
223
|
+
new_spec = File.join(child_dir, "#{new_child_id}-#{slug}.s.md")
|
|
224
|
+
File.rename(spec, new_spec) if spec != new_spec && !File.exist?(new_spec)
|
|
225
|
+
if File.exist?(new_spec)
|
|
226
|
+
update_frontmatter(new_spec) do |fm|
|
|
227
|
+
fm["id"] = new_child_id
|
|
228
|
+
fm["parent"] = new_parent_id
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
rewrite_descendants(child_dir, old_parent_id: old_child_id, new_parent_id: new_child_id, id_map: id_map)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
171
237
|
# Extract the base task ID (without subtask char) from a potentially dotted ID
|
|
172
238
|
# "8pp.t.q7w.a" => "8pp.t.q7w"
|
|
173
239
|
def base_task_id(id)
|
|
@@ -19,8 +19,9 @@ module Ace
|
|
|
19
19
|
# Short reference pattern: "t.q7w" → extract suffix "q7w"
|
|
20
20
|
SHORT_REF_PATTERN = /^[a-z]\.([0-9a-z]{3})$/
|
|
21
21
|
|
|
22
|
-
# Subtask reference pattern: "8pp.t.q7w.a"
|
|
23
|
-
|
|
22
|
+
# Subtask reference pattern: "8pp.t.q7w.a" or nested "8pp.t.q7w.0.a"
|
|
23
|
+
# (parent ID + one or more dot-separated subtask chars)
|
|
24
|
+
SUBTASK_REF_PATTERN = /^([0-9a-z]{3}\.[a-z]\.[0-9a-z]{3}(?:\.[a-z0-9])*)\.([a-z0-9])$/
|
|
24
25
|
|
|
25
26
|
# Short subtask reference: "q7w.a" or "t.q7w.a" (suffix + subtask char)
|
|
26
27
|
SHORT_SUBTASK_REF_PATTERN = /^(?:[a-z]\.)?([0-9a-z]{3})\.([a-z0-9])$/
|
|
@@ -76,7 +77,17 @@ module Ace
|
|
|
76
77
|
# Resolve a subtask reference by finding the parent's scan result,
|
|
77
78
|
# then looking for the subtask folder within that parent's directory.
|
|
78
79
|
def resolve_subtask(parent_id, subtask_char)
|
|
79
|
-
parent_result =
|
|
80
|
+
parent_result = if parent_id.count(".") >= 3
|
|
81
|
+
# Nested parent: resolve level by level from the base task.
|
|
82
|
+
base_id = parent_id.split(".")[0, 3].join(".")
|
|
83
|
+
nested_chars = parent_id.split(".")[3..]
|
|
84
|
+
current = resolve_subtask_chain(base_id, nested_chars)
|
|
85
|
+
return nil unless current
|
|
86
|
+
|
|
87
|
+
current
|
|
88
|
+
else
|
|
89
|
+
@scan_results.find { |sr| sr.id == parent_id }
|
|
90
|
+
end
|
|
80
91
|
return nil unless parent_result
|
|
81
92
|
|
|
82
93
|
subtask_id = "#{parent_id}.#{subtask_char}"
|
|
@@ -109,6 +120,23 @@ module Ace
|
|
|
109
120
|
|
|
110
121
|
nil
|
|
111
122
|
end
|
|
123
|
+
|
|
124
|
+
# Walk nested subtask chars from the base task, returning the final
|
|
125
|
+
# scan result (ids are rebuilt per level exactly like the folders).
|
|
126
|
+
def resolve_subtask_chain(base_id, nested_chars)
|
|
127
|
+
current = @scan_results.find { |sr| sr.id == base_id }
|
|
128
|
+
return nil unless current
|
|
129
|
+
|
|
130
|
+
current_id = base_id
|
|
131
|
+
nested_chars.each do |char|
|
|
132
|
+
next_level = resolve_subtask(current_id, char)
|
|
133
|
+
return nil unless next_level
|
|
134
|
+
|
|
135
|
+
current = next_level
|
|
136
|
+
current_id = next_level.id
|
|
137
|
+
end
|
|
138
|
+
current
|
|
139
|
+
end
|
|
112
140
|
end
|
|
113
141
|
end
|
|
114
142
|
end
|