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.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/composition-rules.yml +2 -17
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +0 -26
- data/.ace-defaults/assign/catalog/steps/create-retro.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/mark-task-done.step.yml +1 -2
- data/.ace-defaults/assign/catalog/steps/onboard.step.yml +0 -17
- data/.ace-defaults/assign/catalog/steps/plan-task.step.yml +0 -11
- data/.ace-defaults/assign/catalog/steps/pre-commit-review.step.yml +3 -0
- data/.ace-defaults/assign/catalog/steps/reflect-and-refactor.step.yml +3 -2
- data/.ace-defaults/assign/catalog/steps/review-pr.step.yml +0 -16
- data/.ace-defaults/assign/catalog/steps/split-subtree-root.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/task-load.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/verify-test-suite.step.yml +7 -34
- data/.ace-defaults/assign/catalog/steps/verify-test.step.yml +7 -4
- data/.ace-defaults/assign/catalog/steps/work-on-task.step.yml +0 -17
- data/.ace-defaults/assign/config.yml +1 -0
- data/.ace-defaults/assign/presets/fix-bug.yml +4 -3
- data/.ace-defaults/assign/presets/quick-implement.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task.yml +6 -16
- data/CHANGELOG.md +216 -0
- data/README.md +20 -43
- data/docs/demo/canonical-skill-source.gif +0 -0
- data/docs/demo/canonical-skill-source.tape.yml +51 -0
- data/docs/demo/fork-provider.cast +834 -0
- data/docs/demo/fork-provider.gif +0 -0
- data/docs/demo/fork-provider.recording.json +30 -0
- data/docs/demo/fork-provider.tape.yml +77 -20
- data/docs/getting-started.md +5 -2
- data/docs/usage.md +74 -4
- data/handbook/guides/fork-context.g.md +31 -7
- data/handbook/skills/as-assign-drive/SKILL.md +13 -1
- data/handbook/skills/as-create-retro-internal/SKILL.md +29 -0
- data/handbook/skills/as-mark-task-done-internal/SKILL.md +29 -0
- data/handbook/skills/as-reflect-and-refactor-internal/SKILL.md +30 -0
- data/handbook/skills/as-task-load-internal/SKILL.md +28 -0
- data/handbook/workflow-instructions/assign/compose.wf.md +3 -3
- data/handbook/workflow-instructions/assign/create-retro-internal.wf.md +11 -0
- data/handbook/workflow-instructions/assign/create.wf.md +6 -3
- data/handbook/workflow-instructions/assign/drive.wf.md +330 -40
- data/handbook/workflow-instructions/assign/mark-task-done-internal.wf.md +12 -0
- data/handbook/workflow-instructions/assign/prepare.wf.md +10 -5
- data/handbook/workflow-instructions/assign/reflect-and-refactor-internal.wf.md +14 -0
- data/handbook/workflow-instructions/assign/run-in-batches.wf.md +4 -1
- data/handbook/workflow-instructions/assign/start.wf.md +5 -2
- data/handbook/workflow-instructions/assign/task-load-internal.wf.md +12 -0
- data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +36 -0
- data/lib/ace/assign/atoms/catalog_loader.rb +105 -2
- data/lib/ace/assign/atoms/preset_expander.rb +4 -0
- data/lib/ace/assign/atoms/step_file_parser.rb +15 -0
- data/lib/ace/assign/atoms/tree_formatter.rb +2 -2
- data/lib/ace/assign/cli/commands/add.rb +20 -11
- data/lib/ace/assign/cli/commands/assignment_target.rb +87 -3
- data/lib/ace/assign/cli/commands/create.rb +1 -1
- data/lib/ace/assign/cli/commands/fail.rb +1 -1
- data/lib/ace/assign/cli/commands/finish.rb +32 -8
- data/lib/ace/assign/cli/commands/fork_run.rb +58 -16
- data/lib/ace/assign/cli/commands/fork_session.rb +52 -0
- data/lib/ace/assign/cli/commands/list.rb +4 -3
- data/lib/ace/assign/cli/commands/retry_cmd.rb +1 -1
- data/lib/ace/assign/cli/commands/start.rb +9 -3
- data/lib/ace/assign/cli/commands/status.rb +237 -230
- data/lib/ace/assign/cli/commands/step.rb +62 -0
- data/lib/ace/assign/cli.rb +8 -1
- data/lib/ace/assign/models/assignment_info.rb +33 -4
- data/lib/ace/assign/models/queue_state.rb +101 -39
- data/lib/ace/assign/models/step.rb +17 -5
- data/lib/ace/assign/molecules/fork_session_launcher.rb +218 -21
- data/lib/ace/assign/molecules/queue_scanner.rb +1 -0
- data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +223 -47
- data/lib/ace/assign/molecules/step_writer.rb +3 -3
- data/lib/ace/assign/molecules/tmux_control_surface_runner.rb +249 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +355 -106
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +1 -0
- metadata +35 -5
- 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
|
data/lib/ace/assign/cli.rb
CHANGED
|
@@ -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 #
|
|
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
|
|
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.
|
|
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
|
-
#
|
|
42
|
+
# Active steps in queue order.
|
|
43
43
|
#
|
|
44
|
-
# @return [
|
|
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 # =>
|
|
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
|
|
27
|
-
#
|
|
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
|
-
|
|
34
|
+
deepest_active
|
|
30
35
|
end
|
|
31
36
|
|
|
32
|
-
# Get all in
|
|
33
|
-
# @return [Array<Step>]
|
|
34
|
-
def
|
|
35
|
-
steps.select { |s| s.status == :
|
|
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
|
|
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
|
|
111
|
-
# - :stalled - Has
|
|
112
|
-
# - :paused - Has pending but no
|
|
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
|
|
120
|
-
return :stalled if
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
|
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]
|
|
211
|
+
# @return [Step, nil] Deepest active step inside subtree, if any
|
|
200
212
|
def current_in_subtree(root_number)
|
|
201
|
-
|
|
213
|
+
deepest_active_in_subtree(root_number)
|
|
202
214
|
end
|
|
203
215
|
|
|
204
|
-
# Get all
|
|
216
|
+
# Get all active steps within a subtree.
|
|
205
217
|
#
|
|
206
218
|
# @param root_number [String] Subtree root step number
|
|
207
|
-
# @return [Array<Step>]
|
|
208
|
-
def
|
|
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 == :
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
|
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, :
|
|
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 == :
|
|
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,
|