ace-assign 0.42.4 → 0.53.4

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/composition-rules.yml +2 -17
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +0 -26
  4. data/.ace-defaults/assign/catalog/steps/create-retro.step.yml +1 -1
  5. data/.ace-defaults/assign/catalog/steps/mark-task-done.step.yml +1 -2
  6. data/.ace-defaults/assign/catalog/steps/onboard.step.yml +0 -17
  7. data/.ace-defaults/assign/catalog/steps/plan-task.step.yml +0 -11
  8. data/.ace-defaults/assign/catalog/steps/pre-commit-review.step.yml +3 -0
  9. data/.ace-defaults/assign/catalog/steps/reflect-and-refactor.step.yml +3 -2
  10. data/.ace-defaults/assign/catalog/steps/review-pr.step.yml +0 -16
  11. data/.ace-defaults/assign/catalog/steps/task-load.step.yml +1 -1
  12. data/.ace-defaults/assign/catalog/steps/verify-test-suite.step.yml +7 -34
  13. data/.ace-defaults/assign/catalog/steps/verify-test.step.yml +7 -4
  14. data/.ace-defaults/assign/catalog/steps/work-on-task.step.yml +0 -17
  15. data/.ace-defaults/assign/presets/fix-bug.yml +4 -3
  16. data/.ace-defaults/assign/presets/quick-implement.yml +1 -1
  17. data/.ace-defaults/assign/presets/work-on-task.yml +3 -16
  18. data/CHANGELOG.md +201 -0
  19. data/README.md +20 -43
  20. data/docs/demo/canonical-skill-source.gif +0 -0
  21. data/docs/demo/canonical-skill-source.tape.yml +51 -0
  22. data/docs/demo/fork-provider.cast +957 -0
  23. data/docs/demo/fork-provider.gif +0 -0
  24. data/docs/demo/fork-provider.recording.json +32 -0
  25. data/docs/demo/fork-provider.tape.yml +65 -20
  26. data/docs/getting-started.md +5 -2
  27. data/docs/usage.md +47 -0
  28. data/handbook/guides/fork-context.g.md +2 -2
  29. data/handbook/skills/as-assign-drive/SKILL.md +13 -1
  30. data/handbook/skills/as-create-retro-internal/SKILL.md +29 -0
  31. data/handbook/skills/as-mark-task-done-internal/SKILL.md +29 -0
  32. data/handbook/skills/as-reflect-and-refactor-internal/SKILL.md +30 -0
  33. data/handbook/skills/as-task-load-internal/SKILL.md +28 -0
  34. data/handbook/workflow-instructions/assign/compose.wf.md +3 -3
  35. data/handbook/workflow-instructions/assign/create-retro-internal.wf.md +11 -0
  36. data/handbook/workflow-instructions/assign/create.wf.md +6 -3
  37. data/handbook/workflow-instructions/assign/drive.wf.md +231 -14
  38. data/handbook/workflow-instructions/assign/mark-task-done-internal.wf.md +12 -0
  39. data/handbook/workflow-instructions/assign/prepare.wf.md +5 -5
  40. data/handbook/workflow-instructions/assign/reflect-and-refactor-internal.wf.md +14 -0
  41. data/handbook/workflow-instructions/assign/run-in-batches.wf.md +4 -1
  42. data/handbook/workflow-instructions/assign/start.wf.md +5 -2
  43. data/handbook/workflow-instructions/assign/task-load-internal.wf.md +12 -0
  44. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +36 -0
  45. data/lib/ace/assign/atoms/catalog_loader.rb +105 -2
  46. data/lib/ace/assign/atoms/step_file_parser.rb +15 -0
  47. data/lib/ace/assign/cli/commands/assignment_target.rb +53 -0
  48. data/lib/ace/assign/cli/commands/finish.rb +7 -4
  49. data/lib/ace/assign/cli/commands/fork_run.rb +4 -1
  50. data/lib/ace/assign/cli/commands/fork_session.rb +52 -0
  51. data/lib/ace/assign/cli/commands/start.rb +9 -3
  52. data/lib/ace/assign/cli/commands/status.rb +208 -227
  53. data/lib/ace/assign/cli/commands/step.rb +62 -0
  54. data/lib/ace/assign/cli.rb +8 -1
  55. data/lib/ace/assign/models/step.rb +4 -2
  56. data/lib/ace/assign/molecules/fork_session_launcher.rb +189 -8
  57. data/lib/ace/assign/molecules/queue_scanner.rb +1 -0
  58. data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +223 -47
  59. data/lib/ace/assign/molecules/tmux_fork_runner.rb +191 -0
  60. data/lib/ace/assign/organisms/assignment_executor.rb +223 -24
  61. data/lib/ace/assign/version.rb +1 -1
  62. metadata +21 -5
  63. data/.ace-defaults/assign/catalog/steps/verify-e2e.step.yml +0 -42
@@ -0,0 +1,36 @@
1
+ ---
2
+ doc-type: workflow
3
+ title: Assign Verify Test Suite Workflow
4
+ purpose: Narrow deterministic verification contract for ace-assign orchestration
5
+ ace-docs:
6
+ last-updated: 2026-04-13
7
+ last-checked: 2026-04-13
8
+ ---
9
+
10
+ # Verify Test Suite Workflow
11
+
12
+ ## Purpose
13
+
14
+ Run the deterministic verification contract used by `ace-assign`:
15
+ - package-scoped `ace-test <package> all --profile 6` for modified packages
16
+ - monorepo verification with `ace-test-suite --target all`
17
+
18
+ Do not run `ace-test-e2e` or `ace-test-e2e-suite` from this workflow.
19
+
20
+ ## Steps
21
+
22
+ 1. Detect whether modified files affect runnable code.
23
+ 2. If changes are docs-only or otherwise non-runnable, skip with a clear reason.
24
+ 3. Detect modified packages from the current diff or working tree.
25
+ 4. Run `ace-test <package> all --profile 6` for each modified package.
26
+ 5. Run `ace-test-suite --target all`.
27
+
28
+ ## Skip Guidance
29
+
30
+ Skip only when all modified files are documentation, retrospectives, task specs, or similarly non-runnable metadata.
31
+
32
+ ## Success Criteria
33
+
34
+ - All modified packages pass `ace-test <package> all --profile 6`
35
+ - `ace-test-suite --target all` passes
36
+ - No E2E commands are used as part of this step
@@ -21,13 +21,19 @@ module Ace
21
21
  # Load all step definitions from a catalog directory.
22
22
  #
23
23
  # @param steps_dir [String] Path to catalog/steps/ directory
24
+ # @param canonical_steps [Array<Hash>, Symbol, Boolean, nil] Canonical
25
+ # step metadata to merge. `:auto` (default) loads from skill sources.
26
+ # `false` disables canonical merge and returns raw YAML definitions.
24
27
  # @return [Array<Hash>] Array of step definition hashes
25
- def self.load_all(steps_dir)
28
+ def self.load_all(steps_dir, canonical_steps: :auto)
26
29
  return [] unless File.directory?(steps_dir)
27
30
 
28
- Dir.glob(File.join(steps_dir, "*.step.yml")).sort.filter_map do |path|
31
+ yaml_steps = Dir.glob(File.join(steps_dir, "*.step.yml")).sort.filter_map do |path|
29
32
  parse_step_file(path)
30
33
  end
34
+ return [] if yaml_steps.empty?
35
+
36
+ merge_step_catalog(yaml_steps, resolve_canonical_steps(canonical_steps))
31
37
  end
32
38
 
33
39
  # Find a step definition by name.
@@ -94,6 +100,103 @@ module Ace
94
100
  warn "Warning: Failed to parse step file #{path}: #{e.message}"
95
101
  nil
96
102
  end
103
+
104
+ def self.resolve_canonical_steps(canonical_steps)
105
+ case canonical_steps
106
+ when false
107
+ []
108
+ when :auto, nil
109
+ begin
110
+ require_relative "../molecules/skill_assign_source_resolver"
111
+ Molecules::SkillAssignSourceResolver.new.assign_step_catalog
112
+ rescue LoadError, StandardError
113
+ []
114
+ end
115
+ when Array
116
+ canonical_steps
117
+ else
118
+ []
119
+ end
120
+ end
121
+ private_class_method :resolve_canonical_steps
122
+
123
+ def self.merge_step_catalog(base_steps, override_steps)
124
+ index = {}
125
+ order = []
126
+
127
+ Array(base_steps).each do |step|
128
+ name = step["name"]
129
+ next if name.nil? || name.empty?
130
+
131
+ index[name] = step
132
+ order << name
133
+ end
134
+
135
+ Array(override_steps).each do |step|
136
+ name = step["name"]
137
+ next if name.nil? || name.empty?
138
+
139
+ order << name unless index.key?(name)
140
+ index[name] = deep_merge_step_definition(index[name], step)
141
+ end
142
+
143
+ order.map { |name| index[name] }.compact
144
+ end
145
+ private_class_method :merge_step_catalog
146
+
147
+ def self.deep_merge_step_definition(base, override)
148
+ return override unless base.is_a?(Hash)
149
+ return base unless override.is_a?(Hash)
150
+
151
+ merged = base.dup
152
+ override.each do |key, value|
153
+ if runtime_binding_override_key?(key, base, override)
154
+ merged[key] = base[key]
155
+ next
156
+ end
157
+
158
+ merged[key] =
159
+ if merged[key].is_a?(Hash) && value.is_a?(Hash)
160
+ deep_merge_step_definition(merged[key], value)
161
+ else
162
+ value
163
+ end
164
+ end
165
+ merged
166
+ end
167
+ private_class_method :deep_merge_step_definition
168
+
169
+ def self.runtime_binding_override_key?(key, base, override)
170
+ return false unless %w[source workflow skill source_skill].include?(key)
171
+ return false unless local_runtime_binding_present?(base)
172
+ canonical_binding_present?(override)
173
+ end
174
+ private_class_method :runtime_binding_override_key?
175
+
176
+ def self.local_runtime_binding_present?(entry)
177
+ entry.is_a?(Hash) && (
178
+ present_string?(entry["source"]) ||
179
+ present_string?(entry["workflow"]) ||
180
+ present_string?(entry["skill"])
181
+ )
182
+ end
183
+ private_class_method :local_runtime_binding_present?
184
+
185
+ def self.canonical_binding_present?(entry)
186
+ entry.is_a?(Hash) && (
187
+ present_string?(entry["source"]) ||
188
+ present_string?(entry["workflow"]) ||
189
+ present_string?(entry["skill"]) ||
190
+ present_string?(entry["source_skill"])
191
+ )
192
+ end
193
+ private_class_method :canonical_binding_present?
194
+
195
+ def self.present_string?(value)
196
+ value.is_a?(String) && !value.strip.empty?
197
+ end
198
+ private_class_method :present_string?
199
+
97
200
  private_class_method :parse_step_file
98
201
  end
99
202
  end
@@ -49,6 +49,7 @@ module Ace
49
49
  {
50
50
  name: fm["name"],
51
51
  status: (fm["status"] || "pending").to_sym,
52
+ source: normalize_source(fm["source"], fm["workflow"], fm["skill"]),
52
53
  skill: fm["skill"],
53
54
  workflow: fm["workflow"],
54
55
  context: context, # "fork" triggers Task tool execution
@@ -212,6 +213,20 @@ module Ace
212
213
  parsed
213
214
  end
214
215
  private_class_method :parse_non_negative_integer
216
+
217
+ def self.normalize_source(source, workflow, skill)
218
+ source_ref = source.to_s.strip
219
+ return source_ref unless source_ref.empty?
220
+
221
+ workflow_ref = workflow.to_s.strip
222
+ return workflow_ref unless workflow_ref.empty?
223
+
224
+ skill_ref = skill.to_s.strip
225
+ return nil if skill_ref.empty?
226
+
227
+ "skill://#{skill_ref}"
228
+ end
229
+ private_class_method :normalize_source
215
230
  end
216
231
  end
217
232
  end
@@ -11,6 +11,7 @@ module Ace
11
11
  # - <assignment-id>@<step-number>
12
12
  module AssignmentTarget
13
13
  Target = Struct.new(:assignment_id, :scope, keyword_init: true)
14
+ View = Struct.new(:assignment, :state, :scoped_state, :current_step, :scope_root, keyword_init: true)
14
15
 
15
16
  private
16
17
 
@@ -48,6 +49,58 @@ module Ace
48
49
  executor.assignment_manager.define_singleton_method(:find_active) { assignment }
49
50
  executor
50
51
  end
52
+
53
+ def resolve_assignment_view(target)
54
+ executor = build_executor_for_target(target)
55
+ result = executor.status
56
+ state = result[:state]
57
+ scoped = scoped_status_view(state, target.scope)
58
+
59
+ View.new(
60
+ assignment: result[:assignment],
61
+ state: state,
62
+ scoped_state: scoped[:state],
63
+ current_step: scoped[:current],
64
+ scope_root: scoped[:root]
65
+ )
66
+ end
67
+
68
+ def scoped_status_view(state, scope)
69
+ return {state: state, current: state.current || state.next_workable, root: nil} if scope.nil? || scope.strip.empty?
70
+
71
+ root = state.find_by_number(scope.strip)
72
+ raise StepErrors::NotFound, "Step #{scope} not found in queue" unless root
73
+
74
+ scoped_steps = state.subtree_steps(root.number)
75
+ scoped_state = Models::QueueState.new(steps: scoped_steps, assignment: state.assignment)
76
+ current = scoped_state.current || scoped_state.next_workable
77
+
78
+ {state: scoped_state, current: current, root: root.number}
79
+ end
80
+
81
+ def fork_scope_root(state, current_step)
82
+ return nil unless current_step
83
+ return current_step if current_step.fork?
84
+
85
+ state.nearest_fork_ancestor(current_step.number)
86
+ end
87
+
88
+ def scoped_fork_metadata_step(state, current_step, scope, scope_root)
89
+ return nil unless current_step
90
+
91
+ if scope && !scope.strip.empty?
92
+ return state.find_by_number(scope_root || scope.strip)
93
+ end
94
+
95
+ fork_scope_root(state, current_step)
96
+ end
97
+
98
+ def effective_fork_provider_for(current_step, scoped_fork_step)
99
+ return nil unless current_step
100
+
101
+ provider = current_step.fork_provider || scoped_fork_step&.fork_provider
102
+ provider.to_s.strip.empty? ? nil : provider
103
+ end
51
104
  end
52
105
  end
53
106
  end
@@ -43,11 +43,8 @@ module Ace
43
43
 
44
44
  if result[:current]
45
45
  puts "Advancing to step #{result[:current].number}: #{result[:current].name}"
46
- puts
47
- puts "Instructions:"
48
- puts result[:current].instructions
46
+ puts "Next: ace-assign step#{step_target_suffix(result[:current].number, options[:assignment])}"
49
47
  else
50
- puts
51
48
  fork_root = target.scope&.strip
52
49
  if fork_root && result[:state].subtree_complete?(fork_root)
53
50
  puts "Fork subtree #{fork_root} completed."
@@ -81,6 +78,12 @@ module Ace
81
78
  rescue IOError, Errno::EBADF
82
79
  nil
83
80
  end
81
+
82
+ def step_target_suffix(step_number, assignment_target)
83
+ return " #{step_number}" if assignment_target.nil? || assignment_target.to_s.strip.empty?
84
+
85
+ %( #{step_number} --assignment "#{assignment_target}")
86
+ end
84
87
  end
85
88
  end
86
89
  end
@@ -21,6 +21,7 @@ module Ace
21
21
  option :provider, desc: "LLM provider:model override (e.g., codex:gpt-5, claude:sonnet)"
22
22
  option :cli_args, desc: "Extra CLI args for provider process"
23
23
  option :timeout, type: :integer, desc: "Execution timeout in seconds"
24
+ option :launch_mode, desc: "Launch mode: auto, headless, or tmux"
24
25
  option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
25
26
  option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
26
27
 
@@ -51,6 +52,7 @@ module Ace
51
52
  puts "Starting fork subtree execution: #{root_step.number} - #{root_step.name}"
52
53
  puts "Assignment: #{assignment.id}"
53
54
  puts "Provider: #{resolved_provider}"
55
+ puts "Launch mode: #{options[:launch_mode] || Molecules::ForkSessionLauncher::DEFAULT_LAUNCH_MODE}"
54
56
  puts "Timeout: #{options[:timeout] || Ace::Assign.config.dig("execution", "timeout") || Molecules::ForkSessionLauncher::DEFAULT_TIMEOUT}s"
55
57
  puts "Next step: #{next_step.number} - #{next_step.name}" if next_step
56
58
  end
@@ -77,7 +79,8 @@ module Ace
77
79
  provider: resolved_provider,
78
80
  cli_args: options[:cli_args],
79
81
  timeout: options[:timeout],
80
- cache_dir: assignment.cache_dir
82
+ cache_dir: assignment.cache_dir,
83
+ launch_mode: options[:launch_mode]
81
84
  )
82
85
  record_fork_pid_info(root_step, launch_result)
83
86
 
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Assign
5
+ module CLI
6
+ module Commands
7
+ # Internal command used by tmux-backed fork panes to launch the provider session once.
8
+ class ForkSession < Ace::Support::Cli::Command
9
+ include Ace::Support::Cli::Base
10
+
11
+ desc "Run one provider-backed fork session for a subtree"
12
+
13
+ option :assignment, required: true, desc: "Target assignment ID"
14
+ option :root, required: true, desc: "Fork subtree root step number"
15
+ option :provider, desc: "LLM provider:model override (e.g., codex:gpt-5, claude:sonnet)"
16
+ option :cli_args, desc: "Extra CLI args for provider process"
17
+ option :timeout, type: :integer, desc: "Execution timeout in seconds"
18
+ option :cache_dir, desc: "Assignment cache directory override"
19
+ option :last_message_file, desc: "Explicit path for fork last-message capture"
20
+ option :session_meta_file, desc: "Explicit path for fork session metadata"
21
+
22
+ def initialize(launcher: nil)
23
+ super()
24
+ @launcher = launcher || Molecules::ForkSessionLauncher.new
25
+ end
26
+
27
+ def call(**options)
28
+ launcher.launch_provider_session(
29
+ assignment_id: options[:assignment],
30
+ fork_root: options[:root],
31
+ provider: options[:provider],
32
+ cli_args: options[:cli_args],
33
+ timeout: options[:timeout],
34
+ cache_dir: options[:cache_dir],
35
+ last_message_file: options[:last_message_file],
36
+ session_meta_file: options[:session_meta_file]
37
+ )
38
+
39
+ 0
40
+ rescue Error, Ace::LLM::Error => e
41
+ warn e.message
42
+ 1
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :launcher
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -29,9 +29,15 @@ module Ace
29
29
 
30
30
  started = result[:started]
31
31
  puts "Step #{started.number} (#{started.name}) started"
32
- puts
33
- puts "Instructions:"
34
- puts started.instructions
32
+ puts "Next: ace-assign step#{step_target_suffix(started.number, options[:assignment])}"
33
+ end
34
+
35
+ private
36
+
37
+ def step_target_suffix(step_number, assignment_target)
38
+ return " #{step_number}" if assignment_target.nil? || assignment_target.to_s.strip.empty?
39
+
40
+ %( #{step_number} --assignment "#{assignment_target}")
35
41
  end
36
42
  end
37
43
  end