ace-assign 0.37.0 → 0.40.3

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.
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/task"
4
+ require "fileutils"
5
+ require "securerandom"
6
+ require "tempfile"
7
+ require "yaml"
8
+
9
+ require_relative "../atoms/preset_loader"
10
+ require_relative "../atoms/preset_expander"
11
+
12
+ module Ace
13
+ module Assign
14
+ module Organisms
15
+ class TaskAssignmentCreator
16
+ DEFAULT_PRESET = "work-on-task"
17
+ SUBTASK_PATTERN = /^[0-9a-z]{3}\.[a-z]\.[0-9a-z]{3}\.[a-z0-9]$/
18
+
19
+ def initialize(task_manager: nil, executor: nil)
20
+ @task_manager = task_manager || Ace::Task::Organisms::TaskManager.new
21
+ @executor = executor || AssignmentExecutor.new
22
+ end
23
+
24
+ def call(task_refs:, preset_name: DEFAULT_PRESET, primary_task_ref: nil)
25
+ requested_refs = normalize_requested_refs(task_refs)
26
+ raise Ace::Support::Cli::Error, "--task requires at least one task reference" if requested_refs.empty?
27
+
28
+ preset = Atoms::PresetLoader.load(preset_name)
29
+ resolved_refs, skipped_terminal = resolve_requested_refs(requested_refs)
30
+
31
+ if resolved_refs.empty?
32
+ raise Ace::Support::Cli::Error,
33
+ "All requested tasks are already terminal (done/skipped/cancelled): #{skipped_terminal.join(', ')}. No assignment created."
34
+ end
35
+
36
+ primary_ref = primary_task_ref.to_s.strip
37
+ primary_ref = resolved_refs.first[:ref] if primary_ref.empty?
38
+
39
+ guard_multi_task_preset!(preset_name, preset, resolved_refs.length)
40
+
41
+ expanded_taskrefs = if preset_supports_taskrefs?(preset)
42
+ expand_task_refs_in_order(resolved_refs)
43
+ else
44
+ [primary_ref]
45
+ end
46
+
47
+ params = build_parameters(preset, primary_ref, expanded_taskrefs)
48
+ validate_parameters!(preset, params)
49
+
50
+ steps = Atoms::PresetExpander.expand(preset, params)
51
+ job_path = write_job_file(
52
+ session_name: "#{preset_name}-#{primary_ref}",
53
+ description: preset["description"],
54
+ steps: steps
55
+ )
56
+
57
+ result = @executor.start(job_path)
58
+ result.merge(
59
+ skipped_terminal: skipped_terminal,
60
+ primary_ref: primary_ref,
61
+ task_refs: expanded_taskrefs,
62
+ job_path: job_path
63
+ )
64
+ end
65
+
66
+ private
67
+
68
+ def normalize_requested_refs(task_refs)
69
+ Array(task_refs)
70
+ .flat_map { |entry| entry.to_s.split(",") }
71
+ .map(&:strip)
72
+ .reject(&:empty?)
73
+ end
74
+
75
+ def resolve_requested_refs(requested_refs)
76
+ resolved = []
77
+ skipped_terminal = []
78
+
79
+ requested_refs.each do |ref|
80
+ task = @task_manager.show(ref.to_s)
81
+ raise Ace::Support::Cli::Error, "Task not found: #{ref}" unless task
82
+
83
+ if task.status.to_s == "draft"
84
+ raise Ace::Support::Cli::Error,
85
+ "Task #{ref} has status 'draft' and has not been reviewed. " \
86
+ "Review the spec first with /as-task-review #{ref} " \
87
+ "(or ace-bundle wfi://task/review), then retry."
88
+ end
89
+
90
+ if Ace::Task::Atoms::TaskValidationRules.terminal_status?(task.status.to_s)
91
+ skipped_terminal << ref.to_s
92
+ next
93
+ end
94
+
95
+ resolved << {ref: ref.to_s, task: task, is_subtask: subtask_ref?(ref)}
96
+ end
97
+
98
+ [resolved, skipped_terminal]
99
+ end
100
+
101
+ def subtask_ref?(ref)
102
+ ref.to_s.match?(SUBTASK_PATTERN)
103
+ end
104
+
105
+ def preset_supports_taskrefs?(preset)
106
+ (preset["parameters"] || {}).key?("taskrefs")
107
+ end
108
+
109
+ def guard_multi_task_preset!(preset_name, preset, input_count)
110
+ return unless input_count > 1 && !preset_supports_taskrefs?(preset)
111
+
112
+ raise Ace::Support::Cli::Error,
113
+ "Preset '#{preset_name}' accepts only single taskref. " \
114
+ "Use a preset with `taskrefs` (e.g., --preset work-on-task)."
115
+ end
116
+
117
+ def expand_task_refs_in_order(resolved_refs)
118
+ resolved_refs.flat_map do |entry|
119
+ ref = entry[:ref]
120
+ task = entry[:task]
121
+
122
+ if entry[:is_subtask]
123
+ [ref]
124
+ elsif task.respond_to?(:subtasks) && task.subtasks&.any?
125
+ active_subtasks = task.subtasks
126
+ .reject { |st| Ace::Task::Atoms::TaskValidationRules.terminal_status?(st.status.to_s) }
127
+ .map(&:id)
128
+ active_subtasks.empty? ? [ref] : active_subtasks
129
+ else
130
+ [ref]
131
+ end
132
+ end
133
+ end
134
+
135
+ def build_parameters(preset, primary_ref, task_refs)
136
+ param_defs = preset["parameters"] || {}
137
+ params = {}
138
+ params["taskref"] = primary_ref if param_defs.key?("taskref")
139
+ params["taskrefs"] = task_refs if param_defs.key?("taskrefs")
140
+ params
141
+ end
142
+
143
+ def validate_parameters!(preset, params)
144
+ errors = Atoms::PresetExpander.validate_parameters(preset, params)
145
+ return if errors.empty?
146
+
147
+ raise Ace::Support::Cli::Error, errors.join(", ")
148
+ end
149
+
150
+ def write_job_file(session_name:, description:, steps:)
151
+ job = {
152
+ "session" => {
153
+ "name" => session_name,
154
+ "description" => description || "Prepared by ace-assign create --task"
155
+ },
156
+ "steps" => steps
157
+ }
158
+
159
+ dir = File.join(Ace::Assign.cache_dir, "jobs")
160
+ FileUtils.mkdir_p(dir)
161
+ path = File.join(dir, "#{session_name}-#{SecureRandom.hex(4)}-job.yml")
162
+ payload = YAML.dump(job)
163
+
164
+ Tempfile.create(["#{session_name}-job", ".yml"], dir) do |tmp|
165
+ tmp.write(payload)
166
+ tmp.flush
167
+ tmp.fsync
168
+ FileUtils.mv(tmp.path, path)
169
+ end
170
+
171
+ path
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Assign
5
- VERSION = "0.37.0"
5
+ VERSION = "0.40.3"
6
6
  end
7
7
  end
data/lib/ace/assign.rb CHANGED
@@ -9,6 +9,7 @@ require "pathname"
9
9
 
10
10
  # CLI and commands
11
11
  require_relative "assign/cli"
12
+ require_relative "assign/organisms/task_assignment_creator"
12
13
 
13
14
  module Ace
14
15
  module Assign
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-assign
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.37.0
4
+ version: 0.40.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
@@ -107,6 +107,20 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0.26'
110
+ - !ruby/object:Gem::Dependency
111
+ name: ace-task
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.18'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.18'
110
124
  - !ruby/object:Gem::Dependency
111
125
  name: ace-support-test-helpers
112
126
  requirement: !ruby/object:Gem::Requirement
@@ -179,6 +193,7 @@ files:
179
193
  - ".ace-defaults/assign/catalog/steps/pre-commit-review.step.yml"
180
194
  - ".ace-defaults/assign/catalog/steps/push-to-remote.step.yml"
181
195
  - ".ace-defaults/assign/catalog/steps/rebase-with-main.step.yml"
196
+ - ".ace-defaults/assign/catalog/steps/record-demo.step.yml"
182
197
  - ".ace-defaults/assign/catalog/steps/reflect-and-refactor.step.yml"
183
198
  - ".ace-defaults/assign/catalog/steps/release-minor.step.yml"
184
199
  - ".ace-defaults/assign/catalog/steps/release.step.yml"
@@ -212,6 +227,7 @@ files:
212
227
  - docs/usage.md
213
228
  - exe/ace-assign
214
229
  - handbook/guides/fork-context.g.md
230
+ - handbook/skills/as-assign-add-task/SKILL.md
215
231
  - handbook/skills/as-assign-compose/SKILL.md
216
232
  - handbook/skills/as-assign-create/SKILL.md
217
233
  - handbook/skills/as-assign-drive/SKILL.md
@@ -219,6 +235,7 @@ files:
219
235
  - handbook/skills/as-assign-recover-fork/SKILL.md
220
236
  - handbook/skills/as-assign-run-in-batches/SKILL.md
221
237
  - handbook/skills/as-assign-start/SKILL.md
238
+ - handbook/workflow-instructions/assign/add-task.wf.md
222
239
  - handbook/workflow-instructions/assign/compose.wf.md
223
240
  - handbook/workflow-instructions/assign/create.wf.md
224
241
  - handbook/workflow-instructions/assign/drive.wf.md
@@ -232,6 +249,8 @@ files:
232
249
  - lib/ace/assign/atoms/composition_rules.rb
233
250
  - lib/ace/assign/atoms/number_generator.rb
234
251
  - lib/ace/assign/atoms/preset_expander.rb
252
+ - lib/ace/assign/atoms/preset_loader.rb
253
+ - lib/ace/assign/atoms/preset_step_resolver.rb
235
254
  - lib/ace/assign/atoms/step_file_parser.rb
236
255
  - lib/ace/assign/atoms/step_numbering.rb
237
256
  - lib/ace/assign/atoms/step_sorter.rb
@@ -255,18 +274,20 @@ files:
255
274
  - lib/ace/assign/molecules/assignment_discoverer.rb
256
275
  - lib/ace/assign/molecules/assignment_manager.rb
257
276
  - lib/ace/assign/molecules/fork_session_launcher.rb
277
+ - lib/ace/assign/molecules/preset_inferrer.rb
258
278
  - lib/ace/assign/molecules/queue_scanner.rb
259
279
  - lib/ace/assign/molecules/skill_assign_source_resolver.rb
260
280
  - lib/ace/assign/molecules/step_renumberer.rb
261
281
  - lib/ace/assign/molecules/step_writer.rb
262
282
  - lib/ace/assign/organisms/assignment_executor.rb
283
+ - lib/ace/assign/organisms/task_assignment_creator.rb
263
284
  - lib/ace/assign/version.rb
264
285
  homepage: https://github.com/cs3b/ace
265
286
  licenses:
266
287
  - MIT
267
288
  metadata:
268
289
  homepage_uri: https://github.com/cs3b/ace
269
- source_code_uri: https://github.com/cs3b/ace
290
+ source_code_uri: https://github.com/cs3b/ace/tree/main/ace-assign/
270
291
  changelog_uri: https://github.com/cs3b/ace/blob/main/ace-assign/CHANGELOG.md
271
292
  rdoc_options: []
272
293
  require_paths: