ace-assign 0.42.4 → 0.55.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.
Files changed (76) 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/split-subtree-root.step.yml +4 -2
  12. data/.ace-defaults/assign/catalog/steps/task-load.step.yml +1 -1
  13. data/.ace-defaults/assign/catalog/steps/verify-test-suite.step.yml +7 -34
  14. data/.ace-defaults/assign/catalog/steps/verify-test.step.yml +7 -4
  15. data/.ace-defaults/assign/catalog/steps/work-on-task.step.yml +0 -17
  16. data/.ace-defaults/assign/config.yml +1 -0
  17. data/.ace-defaults/assign/presets/fix-bug.yml +4 -3
  18. data/.ace-defaults/assign/presets/quick-implement.yml +1 -1
  19. data/.ace-defaults/assign/presets/work-on-task.yml +6 -16
  20. data/CHANGELOG.md +216 -0
  21. data/README.md +20 -43
  22. data/docs/demo/canonical-skill-source.gif +0 -0
  23. data/docs/demo/canonical-skill-source.tape.yml +51 -0
  24. data/docs/demo/fork-provider.cast +834 -0
  25. data/docs/demo/fork-provider.gif +0 -0
  26. data/docs/demo/fork-provider.recording.json +30 -0
  27. data/docs/demo/fork-provider.tape.yml +77 -20
  28. data/docs/getting-started.md +5 -2
  29. data/docs/usage.md +74 -4
  30. data/handbook/guides/fork-context.g.md +31 -7
  31. data/handbook/skills/as-assign-drive/SKILL.md +13 -1
  32. data/handbook/skills/as-create-retro-internal/SKILL.md +29 -0
  33. data/handbook/skills/as-mark-task-done-internal/SKILL.md +29 -0
  34. data/handbook/skills/as-reflect-and-refactor-internal/SKILL.md +30 -0
  35. data/handbook/skills/as-task-load-internal/SKILL.md +28 -0
  36. data/handbook/workflow-instructions/assign/compose.wf.md +3 -3
  37. data/handbook/workflow-instructions/assign/create-retro-internal.wf.md +11 -0
  38. data/handbook/workflow-instructions/assign/create.wf.md +6 -3
  39. data/handbook/workflow-instructions/assign/drive.wf.md +330 -40
  40. data/handbook/workflow-instructions/assign/mark-task-done-internal.wf.md +12 -0
  41. data/handbook/workflow-instructions/assign/prepare.wf.md +10 -5
  42. data/handbook/workflow-instructions/assign/reflect-and-refactor-internal.wf.md +14 -0
  43. data/handbook/workflow-instructions/assign/run-in-batches.wf.md +4 -1
  44. data/handbook/workflow-instructions/assign/start.wf.md +5 -2
  45. data/handbook/workflow-instructions/assign/task-load-internal.wf.md +12 -0
  46. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +36 -0
  47. data/lib/ace/assign/atoms/catalog_loader.rb +105 -2
  48. data/lib/ace/assign/atoms/preset_expander.rb +4 -0
  49. data/lib/ace/assign/atoms/step_file_parser.rb +15 -0
  50. data/lib/ace/assign/atoms/tree_formatter.rb +2 -2
  51. data/lib/ace/assign/cli/commands/add.rb +20 -11
  52. data/lib/ace/assign/cli/commands/assignment_target.rb +87 -3
  53. data/lib/ace/assign/cli/commands/create.rb +1 -1
  54. data/lib/ace/assign/cli/commands/fail.rb +1 -1
  55. data/lib/ace/assign/cli/commands/finish.rb +32 -8
  56. data/lib/ace/assign/cli/commands/fork_run.rb +58 -16
  57. data/lib/ace/assign/cli/commands/fork_session.rb +52 -0
  58. data/lib/ace/assign/cli/commands/list.rb +4 -3
  59. data/lib/ace/assign/cli/commands/retry_cmd.rb +1 -1
  60. data/lib/ace/assign/cli/commands/start.rb +9 -3
  61. data/lib/ace/assign/cli/commands/status.rb +237 -230
  62. data/lib/ace/assign/cli/commands/step.rb +62 -0
  63. data/lib/ace/assign/cli.rb +8 -1
  64. data/lib/ace/assign/models/assignment_info.rb +33 -4
  65. data/lib/ace/assign/models/queue_state.rb +101 -39
  66. data/lib/ace/assign/models/step.rb +17 -5
  67. data/lib/ace/assign/molecules/fork_session_launcher.rb +218 -21
  68. data/lib/ace/assign/molecules/queue_scanner.rb +1 -0
  69. data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +223 -47
  70. data/lib/ace/assign/molecules/step_writer.rb +3 -3
  71. data/lib/ace/assign/molecules/tmux_control_surface_runner.rb +249 -0
  72. data/lib/ace/assign/organisms/assignment_executor.rb +355 -106
  73. data/lib/ace/assign/version.rb +1 -1
  74. data/lib/ace/assign.rb +1 -0
  75. metadata +35 -5
  76. data/.ace-defaults/assign/catalog/steps/verify-e2e.step.yml +0 -42
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Assign
5
+ module CLI
6
+ module Commands
7
+ # Print instructions for the focused active, next, or an explicit step.
8
+ class Step < Ace::Support::Cli::Command
9
+ include Ace::Support::Cli::Base
10
+ include AssignmentTarget
11
+
12
+ desc "Show instructions for the active, next, or explicit step"
13
+
14
+ argument :step, required: false, desc: "Exact step number to inspect"
15
+ option :assignment, desc: "Target specific assignment ID"
16
+ option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
17
+ option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
18
+
19
+ def call(step: nil, **options)
20
+ target = resolve_assignment_target(options)
21
+ view = resolve_assignment_view(target)
22
+ inspected = resolve_step(view, step, target)
23
+
24
+ return if options[:quiet]
25
+
26
+ if inspected
27
+ puts inspected.instructions
28
+ else
29
+ puts no_work_message(view)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def resolve_step(view, explicit_step, target)
36
+ if explicit_step && !explicit_step.to_s.strip.empty?
37
+ step = view.state.find_by_number(explicit_step)
38
+ raise StepErrors::NotFound, "Step #{explicit_step} not found in queue" unless step
39
+
40
+ if target.scope && !target.scope.strip.empty? && !view.scoped_state.in_subtree?(target.scope, step.number)
41
+ raise StepErrors::NotFound, "Step #{explicit_step} is outside subtree #{target.scope}"
42
+ end
43
+
44
+ return step
45
+ end
46
+
47
+ view.focus_step
48
+ end
49
+
50
+ def no_work_message(view)
51
+ state_label = view.scoped_state.assignment_state.to_s
52
+ last_done = view.scoped_state.last_done ? "#{view.scoped_state.last_done.number} #{view.scoped_state.last_done.name}" : "none"
53
+ [
54
+ "Assignment: #{view.assignment.id} | Status: #{state_label} | Progress: #{view.scoped_state.done.size}/#{view.scoped_state.size} done",
55
+ "Last done: #{last_done} | No active or next workable step"
56
+ ].join("\n")
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -31,6 +31,7 @@ require_relative "molecules/step_writer"
31
31
  require_relative "molecules/step_renumberer"
32
32
  require_relative "molecules/skill_assign_source_resolver"
33
33
  require_relative "molecules/fork_session_launcher"
34
+ require_relative "molecules/tmux_control_surface_runner"
34
35
  require_relative "molecules/preset_inferrer"
35
36
 
36
37
  # Organisms
@@ -40,6 +41,7 @@ require_relative "organisms/assignment_executor"
40
41
  require_relative "cli/commands/create"
41
42
  require_relative "cli/commands/assignment_target"
42
43
  require_relative "cli/commands/status"
44
+ require_relative "cli/commands/step"
43
45
  require_relative "cli/commands/start"
44
46
  require_relative "cli/commands/finish"
45
47
  require_relative "cli/commands/fail"
@@ -48,6 +50,7 @@ require_relative "cli/commands/retry_cmd"
48
50
  require_relative "cli/commands/list"
49
51
  require_relative "cli/commands/select"
50
52
  require_relative "cli/commands/fork_run"
53
+ require_relative "cli/commands/fork_session"
51
54
 
52
55
  module Ace
53
56
  module Assign
@@ -61,6 +64,7 @@ module Ace
61
64
  REGISTERED_COMMANDS = [
62
65
  ["create", "Create assignment from preset or YAML"],
63
66
  ["status", "Show assignment status"],
67
+ ["step", "Show step instructions"],
64
68
  ["start", "Start next workable step"],
65
69
  ["finish", "Complete current step with report"],
66
70
  ["fail", "Mark step as failed"],
@@ -73,7 +77,8 @@ module Ace
73
77
 
74
78
  HELP_EXAMPLES = [
75
79
  "ace-assign create --preset review # Start review assignment",
76
- "ace-assign status # Current step progress",
80
+ "ace-assign status # Compact queue progress",
81
+ "ace-assign step # Current or next step instructions",
77
82
  "ace-assign start # Start next workable step",
78
83
  "ace-assign finish --message done.md # Complete active step",
79
84
  "cat report.md | ace-assign finish # Complete step via stdin",
@@ -115,6 +120,7 @@ module Ace
115
120
  # Register commands (wrapped to capture exit codes)
116
121
  register "create", wrap_command(Commands::Create)
117
122
  register "status", wrap_command(Commands::Status)
123
+ register "step", wrap_command(Commands::Step)
118
124
  register "start", wrap_command(Commands::Start)
119
125
  register "finish", wrap_command(Commands::Finish)
120
126
  register "fail", wrap_command(Commands::Fail)
@@ -123,6 +129,7 @@ module Ace
123
129
  register "list", wrap_command(Commands::List)
124
130
  register "select", wrap_command(Commands::Select)
125
131
  register "fork-run", wrap_command(Commands::ForkRun)
132
+ register "fork-session", wrap_command(Commands::ForkSession)
126
133
 
127
134
  # Register version command
128
135
  version_cmd = Ace::Support::Cli::VersionCommand.build(
@@ -7,13 +7,13 @@ module Ace
7
7
  #
8
8
  # Pure data carrier with computed state (ATOM pattern).
9
9
  # Wraps an assignment with its queue state to provide
10
- # computed state, progress, and current step information.
10
+ # computed state, progress, and active step information.
11
11
  #
12
12
  # @example
13
13
  # info = AssignmentInfo.new(assignment: assignment, queue_state: state)
14
14
  # info.state # => :running
15
15
  # info.progress # => "2/5"
16
- # info.current_step # => "020-implement"
16
+ # info.step_focus # => "implement"
17
17
  class AssignmentInfo
18
18
  attr_reader :assignment, :queue_state
19
19
 
@@ -39,13 +39,42 @@ module Ace
39
39
  "#{s[:done]}/#{s[:total]}"
40
40
  end
41
41
 
42
- # Current step display string
42
+ # Active steps in queue order.
43
43
  #
44
- # @return [String] Current step name or "-"
44
+ # @return [Array<Step>] Active steps
45
+ def active_steps
46
+ queue_state.active_steps
47
+ end
48
+
49
+ # Next pending workable step when nothing is active.
50
+ #
51
+ # @return [Step, nil] Next pending step or nil
52
+ def next_step
53
+ return nil unless active_steps.empty?
54
+
55
+ queue_state.next_workable
56
+ end
57
+
58
+ # Deterministic active step name kept for internal callers.
59
+ #
60
+ # @return [String] Focused active step name or "-"
45
61
  def current_step
46
62
  queue_state.current&.name || "-"
47
63
  end
48
64
 
65
+ # Human-friendly active/next summary for compact tables.
66
+ #
67
+ # @return [String] Active step names or next-step hint
68
+ def step_focus
69
+ if active_steps.any?
70
+ active_steps.map(&:name).join(", ")
71
+ elsif next_step
72
+ "next: #{next_step.name}"
73
+ else
74
+ "-"
75
+ end
76
+ end
77
+
49
78
  # Check if assignment is completed
50
79
  #
51
80
  # @return [Boolean]
@@ -10,7 +10,7 @@ module Ace
10
10
  #
11
11
  # @example
12
12
  # state = QueueState.new(steps: steps, assignment: assignment)
13
- # state.current # => Step with status :in_progress
13
+ # state.current # => Deepest active step
14
14
  # state.pending # => Array of pending steps
15
15
  class QueueState
16
16
  attr_reader :steps, :assignment
@@ -23,16 +23,21 @@ module Ace
23
23
  @children_index = build_children_index(steps)
24
24
  end
25
25
 
26
- # Get current in-progress step
27
- # @return [Step, nil] Current step or nil
26
+ # Get the deterministic single-step active target.
27
+ #
28
+ # Returns the deepest active step in queue order. When several active
29
+ # branches exist globally, this is an internal convenience only; public
30
+ # status should use active_steps.
31
+ #
32
+ # @return [Step, nil] Deepest active step or nil
28
33
  def current
29
- in_progress_steps.first
34
+ deepest_active
30
35
  end
31
36
 
32
- # Get all in-progress steps
33
- # @return [Array<Step>] In-progress steps
34
- def in_progress_steps
35
- steps.select { |s| s.status == :in_progress }
37
+ # Get all active steps in queue order.
38
+ # @return [Array<Step>] Active steps
39
+ def active_steps
40
+ steps.select { |s| s.status == :active }
36
41
  end
37
42
 
38
43
  # Get all pending steps
@@ -65,7 +70,7 @@ module Ace
65
70
  steps.empty?
66
71
  end
67
72
 
68
- # Check if all steps are complete (no pending or in_progress)
73
+ # Check if all steps are complete (no pending or active)
69
74
  # @return [Boolean]
70
75
  def complete?
71
76
  steps.all?(&:complete?)
@@ -107,28 +112,28 @@ module Ace
107
112
  # - :empty - No steps in queue
108
113
  # - :completed - All steps complete (done or failed)
109
114
  # - :failed - Has failed step(s) but NOT all complete (stuck)
110
- # - :running - Has in_progress step with recent activity (< 1 hour)
111
- # - :stalled - Has in_progress step but stale (> 1 hour)
112
- # - :paused - Has pending but no in_progress (interrupted)
115
+ # - :running - Has active step(s) with recent activity (< 1 hour)
116
+ # - :stalled - Has active step(s) but all are stale (> 1 hour)
117
+ # - :paused - Has pending but no active step (interrupted)
113
118
  #
114
119
  # @return [Symbol] Assignment state
115
120
  def assignment_state
116
121
  return :empty if empty?
117
122
  return :completed if complete?
118
123
  return :failed if failed.any?
119
- return :running if current && recently_active?
120
- return :stalled if current
124
+ return :running if active_steps.any? && recently_active?
125
+ return :stalled if active_steps.any?
121
126
 
122
127
  :paused
123
128
  end
124
129
 
125
- # Check if the current in_progress step has recent activity
130
+ # Check if any active step has recent activity.
126
131
  # @param threshold [Integer] Seconds since started_at to consider active (default: 1 hour)
127
132
  # @return [Boolean]
128
133
  def recently_active?(threshold: 3600)
129
- return false unless current&.started_at
130
-
131
- (Time.now - current.started_at) < threshold
134
+ active_steps.any? do |step|
135
+ step.started_at && ((Time.now - step.started_at) < threshold)
136
+ end
132
137
  end
133
138
 
134
139
  # Summary for display
@@ -137,12 +142,19 @@ module Ace
137
142
  {
138
143
  total: size,
139
144
  done: done.size,
140
- in_progress: in_progress_steps.size,
145
+ active: active_steps.size,
141
146
  pending: pending.size,
142
147
  failed: failed.size
143
148
  }
144
149
  end
145
150
 
151
+ # Get deepest active step in queue order.
152
+ #
153
+ # @return [Step, nil] Deepest active step
154
+ def deepest_active
155
+ deepest_active_from(active_steps)
156
+ end
157
+
146
158
  # Get all direct children of a step (O(1) via index)
147
159
  # @param parent_number [String] Parent step number
148
160
  # @return [Array<Step>] Direct child steps
@@ -193,21 +205,29 @@ module Ace
193
205
  subtree_steps(root_number).any? { |s| s.status == :failed }
194
206
  end
195
207
 
196
- # Get the current in-progress step within a subtree.
208
+ # Get the deterministic active target within a subtree.
197
209
  #
198
210
  # @param root_number [String] Subtree root step number
199
- # @return [Step, nil] In-progress step inside subtree, if any
211
+ # @return [Step, nil] Deepest active step inside subtree, if any
200
212
  def current_in_subtree(root_number)
201
- in_progress_in_subtree(root_number).first
213
+ deepest_active_in_subtree(root_number)
202
214
  end
203
215
 
204
- # Get all in-progress steps within a subtree.
216
+ # Get all active steps within a subtree.
205
217
  #
206
218
  # @param root_number [String] Subtree root step number
207
- # @return [Array<Step>] In-progress steps inside subtree
208
- def in_progress_in_subtree(root_number)
219
+ # @return [Array<Step>] Active steps inside subtree
220
+ def active_in_subtree(root_number)
209
221
  subtree_steps(root_number)
210
- .select { |s| s.status == :in_progress }
222
+ .select { |s| s.status == :active }
223
+ end
224
+
225
+ # Get the deepest active step within a subtree.
226
+ #
227
+ # @param root_number [String] Subtree root step number
228
+ # @return [Step, nil] Deepest active step in subtree
229
+ def deepest_active_in_subtree(root_number)
230
+ deepest_active_from(active_in_subtree(root_number))
211
231
  end
212
232
 
213
233
  # Get next workable step constrained to a subtree.
@@ -215,10 +235,7 @@ module Ace
215
235
  # @param root_number [String] Subtree root step number
216
236
  # @return [Step, nil] Next pending workable step inside subtree
217
237
  def next_workable_in_subtree(root_number)
218
- subtree_steps(root_number)
219
- .select { |s| s.status == :pending }
220
- .reject { |s| has_incomplete_children?(s.number) }
221
- .first
238
+ next_workable(scope_root: root_number)
222
239
  end
223
240
 
224
241
  # Build ancestor chain from closest parent to root.
@@ -260,19 +277,41 @@ module Ace
260
277
  children_of(parent_number).any? { |s| s.status != :done }
261
278
  end
262
279
 
263
- # Get next workable step considering hierarchy.
280
+ # Get next workable pending step considering hierarchy and active fork ownership.
264
281
  # A step is workable if it's pending and has no incomplete children.
265
- # Prefers children of current/recent work.
282
+ #
283
+ # Pending descendants under an active fork root are hidden unless the caller
284
+ # is explicitly scoped inside that root.
285
+ #
286
+ # @param scope_root [String, nil] Optional subtree scope
266
287
  # @return [Step, nil] Next step to work on
267
- def next_workable
268
- # First, find pending steps
269
- pending_steps = pending
288
+ def next_workable(scope_root: nil)
289
+ candidate_steps = scope_root ? subtree_steps(scope_root) : steps
290
+ candidate_steps
291
+ .select { |s| s.status == :pending }
292
+ .reject { |s| has_incomplete_children?(s.number) && !runnable_delegation_parent?(s, scope_root: scope_root) }
293
+ .reject { |s| hidden_by_active_fork_root?(s.number, scope_root: scope_root) }
294
+ .first
295
+ end
270
296
 
271
- # Filter to steps that don't have incomplete children
272
- workable = pending_steps.reject { |s| has_incomplete_children?(s.number) }
297
+ # Get active fork roots in queue order.
298
+ #
299
+ # @return [Array<Step>] Active fork-scoped steps
300
+ def active_fork_roots
301
+ active_steps.select(&:fork?)
302
+ end
273
303
 
274
- # Return first workable step (already sorted by number)
275
- workable.first
304
+ # Check whether active steps inside a subtree fan out across more than one branch.
305
+ #
306
+ # @param root_number [String] Subtree root step number
307
+ # @param extra_active [Array<String>] Additional active step numbers to include
308
+ # @return [Boolean] True when active steps inside the subtree are not on a single path
309
+ def active_branch_conflict_in_subtree?(root_number, extra_active: [])
310
+ active_numbers = active_in_subtree(root_number).map(&:number) + Array(extra_active).map(&:to_s)
311
+ unique_active = active_numbers.uniq
312
+ unique_active.combination(2).any? do |left, right|
313
+ !(in_subtree?(left, right) || in_subtree?(right, left))
314
+ end
276
315
  end
277
316
 
278
317
  # Get all step numbers as an array
@@ -295,6 +334,12 @@ module Ace
295
334
 
296
335
  private
297
336
 
337
+ def runnable_delegation_parent?(step, scope_root:)
338
+ return false unless scope_root.nil? || scope_root.to_s.strip.empty?
339
+
340
+ step.batch_parent == true || step.fork?
341
+ end
342
+
298
343
  # Build index of children by parent number for O(1) lookups
299
344
  # @param steps [Array<Step>] All steps
300
345
  # @return [Hash<String, Array<Step>>] Parent number => children mapping
@@ -321,6 +366,23 @@ module Ace
321
366
  }
322
367
  end
323
368
  end
369
+
370
+ def deepest_active_from(active)
371
+ return nil if active.empty?
372
+
373
+ max_depth = active.map { |step| step.number.to_s.split(".").length }.max
374
+ active.find { |step| step.number.to_s.split(".").length == max_depth }
375
+ end
376
+
377
+ def hidden_by_active_fork_root?(step_number, scope_root:)
378
+ active_fork_roots.any? do |root|
379
+ next false if step_number == root.number
380
+ next false unless in_subtree?(root.number, step_number)
381
+ next false if scope_root && in_subtree?(root.number, scope_root)
382
+
383
+ true
384
+ end
385
+ end
324
386
  end
325
387
  end
326
388
  end
@@ -17,13 +17,13 @@ module Ace
17
17
  # )
18
18
  class Step
19
19
  # Valid status values
20
- STATUSES = %i[pending in_progress done failed].freeze
20
+ STATUSES = %i[pending active done failed].freeze
21
21
 
22
22
  # Valid context values for execution context
23
23
  VALID_CONTEXTS = %w[fork].freeze
24
24
 
25
25
  attr_reader :number, :name, :status, :instructions, :report, :error,
26
- :started_at, :completed_at, :added_by, :parent, :file_path, :skill, :context,
26
+ :started_at, :completed_at, :added_by, :parent, :file_path, :source, :skill, :context,
27
27
  :workflow,
28
28
  :batch_parent, :parallel, :max_parallel, :fork_retry_limit, :fork_options,
29
29
  :fork_launch_pid, :fork_tracked_pids, :fork_pid_updated_at, :fork_pid_file,
@@ -31,7 +31,7 @@ module Ace
31
31
 
32
32
  # @param number [String] Step number (e.g., "010", "010.01")
33
33
  # @param name [String] Step name
34
- # @param status [Symbol] Step status (:pending, :in_progress, :done, :failed)
34
+ # @param status [Symbol] Step status (:pending, :active, :done, :failed)
35
35
  # @param instructions [String] Step instructions (markdown)
36
36
  # @param report [String, nil] Completion report content
37
37
  # @param error [String, nil] Error message (if failed)
@@ -54,7 +54,7 @@ module Ace
54
54
  # @param stall_reason [String, nil] Last agent message captured when fork stalled
55
55
  def initialize(number:, name:, status:, instructions:, report: nil, error: nil,
56
56
  started_at: nil, completed_at: nil, added_by: nil, parent: nil,
57
- file_path: nil, skill: nil, workflow: nil, context: nil,
57
+ file_path: nil, source: nil, skill: nil, workflow: nil, context: nil,
58
58
  batch_parent: nil, parallel: nil, max_parallel: nil, fork_retry_limit: nil,
59
59
  fork_options: nil,
60
60
  fork_launch_pid: nil, fork_tracked_pids: nil, fork_pid_updated_at: nil,
@@ -78,6 +78,7 @@ module Ace
78
78
  @added_by = added_by&.freeze
79
79
  @parent = parent&.freeze
80
80
  @file_path = file_path&.freeze
81
+ @source = source&.freeze
81
82
  @skill = skill&.freeze
82
83
  @workflow = workflow&.freeze
83
84
  @context = context&.freeze
@@ -102,7 +103,7 @@ module Ace
102
103
  # Check if step can be worked on
103
104
  # @return [Boolean]
104
105
  def workable?
105
- status == :pending || status == :in_progress
106
+ status == :pending || status == :active
106
107
  end
107
108
 
108
109
  # Check if this is a retry of another step
@@ -127,6 +128,16 @@ module Ace
127
128
  provider.empty? ? nil : provider
128
129
  end
129
130
 
131
+ # Resolve per-step launch-mode override from fork options.
132
+ # @return [String, nil]
133
+ def fork_mode
134
+ return nil unless fork_options
135
+
136
+ mode = fork_options["mode"]
137
+ mode = mode.to_s.strip
138
+ mode.empty? ? nil : mode
139
+ end
140
+
130
141
  # Get the original step number if this is a retry
131
142
  # @return [String, nil] Original step number
132
143
  def retry_of
@@ -141,6 +152,7 @@ module Ace
141
152
  {
142
153
  "name" => name,
143
154
  "status" => status.to_s,
155
+ "source" => source,
144
156
  "skill" => skill,
145
157
  "workflow" => workflow,
146
158
  "context" => context,