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.
- 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/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/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 +3 -16
- data/CHANGELOG.md +201 -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 +957 -0
- data/docs/demo/fork-provider.gif +0 -0
- data/docs/demo/fork-provider.recording.json +32 -0
- data/docs/demo/fork-provider.tape.yml +65 -20
- data/docs/getting-started.md +5 -2
- data/docs/usage.md +47 -0
- data/handbook/guides/fork-context.g.md +2 -2
- 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 +231 -14
- data/handbook/workflow-instructions/assign/mark-task-done-internal.wf.md +12 -0
- data/handbook/workflow-instructions/assign/prepare.wf.md +5 -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/step_file_parser.rb +15 -0
- data/lib/ace/assign/cli/commands/assignment_target.rb +53 -0
- data/lib/ace/assign/cli/commands/finish.rb +7 -4
- data/lib/ace/assign/cli/commands/fork_run.rb +4 -1
- data/lib/ace/assign/cli/commands/fork_session.rb +52 -0
- data/lib/ace/assign/cli/commands/start.rb +9 -3
- data/lib/ace/assign/cli/commands/status.rb +208 -227
- data/lib/ace/assign/cli/commands/step.rb +62 -0
- data/lib/ace/assign/cli.rb +8 -1
- data/lib/ace/assign/models/step.rb +4 -2
- data/lib/ace/assign/molecules/fork_session_launcher.rb +189 -8
- 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/tmux_fork_runner.rb +191 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +223 -24
- data/lib/ace/assign/version.rb +1 -1
- metadata +21 -5
- data/.ace-defaults/assign/catalog/steps/verify-e2e.step.yml +0 -42
|
@@ -6,27 +6,11 @@ module Ace
|
|
|
6
6
|
module Assign
|
|
7
7
|
module CLI
|
|
8
8
|
module Commands
|
|
9
|
-
# Display current queue status
|
|
10
|
-
#
|
|
11
|
-
# Shows the work queue with hierarchical step structure.
|
|
12
|
-
# Nested steps are indented to show parent-child relationships.
|
|
13
|
-
#
|
|
14
|
-
# @example Basic usage
|
|
15
|
-
# ace-assign status
|
|
16
|
-
#
|
|
17
|
-
# @example Flat output (no hierarchy)
|
|
18
|
-
# ace-assign status --flat
|
|
19
|
-
#
|
|
20
|
-
# @example Status for specific assignment
|
|
21
|
-
# ace-assign status --assignment abc123
|
|
22
|
-
#
|
|
23
|
-
# @example Show all assignments including completed
|
|
24
|
-
# ace-assign status --all
|
|
9
|
+
# Display current queue status.
|
|
25
10
|
class Status < Ace::Support::Cli::Command
|
|
26
11
|
include Ace::Support::Cli::Base
|
|
27
12
|
include AssignmentTarget
|
|
28
13
|
|
|
29
|
-
# Status icons for consistent display
|
|
30
14
|
STATUS_ICONS = {
|
|
31
15
|
done: "✓ Done",
|
|
32
16
|
in_progress: "▶ Active",
|
|
@@ -34,24 +18,26 @@ module Ace
|
|
|
34
18
|
failed: "✗ Failed"
|
|
35
19
|
}.freeze
|
|
36
20
|
|
|
37
|
-
# State labels for other assignments section
|
|
38
21
|
STATE_LABELS = {
|
|
39
22
|
running: "running",
|
|
40
23
|
paused: "paused",
|
|
41
24
|
completed: "completed",
|
|
42
25
|
failed: "failed",
|
|
43
|
-
empty: "empty"
|
|
26
|
+
empty: "empty",
|
|
27
|
+
stalled: "stalled"
|
|
44
28
|
}.freeze
|
|
29
|
+
PROGRESS_BAR_WIDTH = 10
|
|
45
30
|
|
|
46
|
-
# Column widths for hierarchical display
|
|
47
31
|
COL_NUMBER = 12
|
|
48
32
|
COL_STATUS = 12
|
|
49
33
|
COL_NAME = 30
|
|
50
34
|
COL_FORK = 6
|
|
35
|
+
PREVIEW_LIMIT = 5
|
|
51
36
|
|
|
52
37
|
desc "Display current workflow queue status"
|
|
53
38
|
|
|
54
|
-
option :flat, aliases: ["-f"], type: :boolean, default: false, desc: "Show flat list (
|
|
39
|
+
option :flat, aliases: ["-f"], type: :boolean, default: false, desc: "Show flat list (full mode only)"
|
|
40
|
+
option :mode, desc: "Text output mode (compact, progress, full)", default: "compact"
|
|
55
41
|
option :format, desc: "Output format (table, json)", default: "table"
|
|
56
42
|
option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
|
|
57
43
|
option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
|
|
@@ -60,81 +46,43 @@ module Ace
|
|
|
60
46
|
|
|
61
47
|
def call(**options)
|
|
62
48
|
target = resolve_assignment_target(options)
|
|
49
|
+
view = resolve_assignment_view(target)
|
|
63
50
|
|
|
64
|
-
|
|
65
|
-
result = executor.status
|
|
66
|
-
state = result[:state]
|
|
67
|
-
assignment = result[:assignment]
|
|
68
|
-
scoped = scoped_status_view(state, target.scope)
|
|
69
|
-
scoped_state = scoped[:state]
|
|
70
|
-
current_for_display = scoped[:current]
|
|
71
|
-
scope_root = scoped[:root]
|
|
72
|
-
|
|
73
|
-
unless options[:quiet]
|
|
74
|
-
if options[:format] == "json"
|
|
75
|
-
scoped_fork_step = scoped_fork_metadata_step(state, current_for_display, target.scope, scope_root)
|
|
76
|
-
puts JSON.pretty_generate(status_to_h(assignment, scoped_state, current_for_display, scoped_fork_step: scoped_fork_step))
|
|
77
|
-
return
|
|
78
|
-
end
|
|
51
|
+
return if options[:quiet]
|
|
79
52
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
puts "Current Step: #{current_for_display.number} - #{current_for_display.name}"
|
|
88
|
-
puts "Current Status: #{current_for_display.status}"
|
|
89
|
-
if current_for_display.stall_reason
|
|
90
|
-
lines = current_for_display.stall_reason.to_s.strip.lines
|
|
91
|
-
puts "Stall Reason: #{lines.first&.chomp}"
|
|
92
|
-
lines[1..].each { |l| puts " #{l.chomp}" } if lines.length > 1
|
|
93
|
-
print_hitl_stall_guidance(lines.first.to_s)
|
|
94
|
-
end
|
|
95
|
-
if current_for_display.workflow
|
|
96
|
-
puts "Workflow: #{current_for_display.workflow}"
|
|
97
|
-
elsif current_for_display.skill
|
|
98
|
-
puts "Skill: #{current_for_display.skill}"
|
|
99
|
-
end
|
|
100
|
-
if current_for_display.context
|
|
101
|
-
puts "Context: #{current_for_display.context}"
|
|
102
|
-
end
|
|
103
|
-
effective_fork_provider = effective_fork_provider_for(current_for_display, scoped_fork_step)
|
|
104
|
-
if effective_fork_provider
|
|
105
|
-
puts "Fork Provider: #{effective_fork_provider}"
|
|
106
|
-
end
|
|
107
|
-
puts
|
|
108
|
-
print_scoped_fork_pid_info(scoped_fork_step)
|
|
109
|
-
|
|
110
|
-
if current_for_display.fork? && %i[pending in_progress].include?(current_for_display.status)
|
|
111
|
-
# Fork context: output Task tool instructions
|
|
112
|
-
print_fork_instructions(current_for_display, assignment)
|
|
113
|
-
else
|
|
114
|
-
puts "Instructions:"
|
|
115
|
-
puts current_for_display.instructions
|
|
116
|
-
|
|
117
|
-
if fork_root && (target.scope.nil? || target.scope.strip.empty?)
|
|
118
|
-
puts
|
|
119
|
-
puts "Fork subtree detected (root: #{fork_root.number} - #{fork_root.name})."
|
|
120
|
-
puts "Run in forked process:"
|
|
121
|
-
puts " ace-assign fork-run --root #{fork_root.number} --assignment #{assignment.id}"
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
elsif scoped_state.complete?
|
|
125
|
-
puts
|
|
126
|
-
puts "Assignment completed!"
|
|
127
|
-
end
|
|
53
|
+
if options[:format] == "json"
|
|
54
|
+
scoped_fork_step = scoped_fork_metadata_step(view.state, view.current_step, target.scope, view.scope_root)
|
|
55
|
+
puts JSON.pretty_generate(
|
|
56
|
+
status_to_h(view.assignment, view.scoped_state, view.current_step, scoped_fork_step: scoped_fork_step)
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
end
|
|
128
60
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
61
|
+
mode = normalize_mode(options[:mode])
|
|
62
|
+
raise Ace::Support::Cli::Error, "--flat is supported only with --mode full" if options[:flat] && mode != "full"
|
|
63
|
+
raise Ace::Support::Cli::Error, "--all is supported only with --mode full or compact" if options[:all] && mode == "progress"
|
|
64
|
+
|
|
65
|
+
case mode
|
|
66
|
+
when "progress"
|
|
67
|
+
puts progress_summary_line(view.assignment, view.scoped_state, view.current_step)
|
|
68
|
+
when "full"
|
|
69
|
+
print_full_status(view, target, flat: options[:flat], include_completed: options[:all])
|
|
70
|
+
else
|
|
71
|
+
print_compact_status(view, target, include_completed: options[:all])
|
|
133
72
|
end
|
|
134
73
|
end
|
|
135
74
|
|
|
136
75
|
private
|
|
137
76
|
|
|
77
|
+
def normalize_mode(value)
|
|
78
|
+
mode = value.to_s.strip
|
|
79
|
+
mode = "compact" if mode.empty?
|
|
80
|
+
allowed = %w[compact progress full]
|
|
81
|
+
raise Ace::Support::Cli::Error, "Unsupported status mode '#{mode}'. Use one of: #{allowed.join(', ')}." unless allowed.include?(mode)
|
|
82
|
+
|
|
83
|
+
mode
|
|
84
|
+
end
|
|
85
|
+
|
|
138
86
|
def status_to_h(assignment, state, current_step, scoped_fork_step: nil)
|
|
139
87
|
{
|
|
140
88
|
assignment: {
|
|
@@ -143,7 +91,10 @@ module Ace
|
|
|
143
91
|
state: state.assignment_state.to_s
|
|
144
92
|
},
|
|
145
93
|
steps: state.steps.map { |step| step_to_h(step) },
|
|
146
|
-
current_step: step_to_h(
|
|
94
|
+
current_step: step_to_h(
|
|
95
|
+
current_step,
|
|
96
|
+
effective_fork_provider: effective_fork_provider_for(current_step, scoped_fork_step)
|
|
97
|
+
),
|
|
147
98
|
progress: "#{state.done.size}/#{state.size} done"
|
|
148
99
|
}
|
|
149
100
|
end
|
|
@@ -167,17 +118,149 @@ module Ace
|
|
|
167
118
|
}.compact
|
|
168
119
|
end
|
|
169
120
|
|
|
170
|
-
def
|
|
171
|
-
|
|
121
|
+
def print_compact_status(view, target, include_completed:)
|
|
122
|
+
lines = []
|
|
123
|
+
lines.concat(compact_summary_lines(view.assignment, view.scoped_state, view.current_step))
|
|
124
|
+
|
|
125
|
+
unless target.assignment_id
|
|
126
|
+
other_line = compact_other_assignments_line(view.assignment.id, include_completed: include_completed)
|
|
127
|
+
lines << other_line if other_line
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
puts lines.take(10).join("\n")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def compact_summary_lines(assignment, state, current_step)
|
|
134
|
+
lines = [
|
|
135
|
+
compact_assignment_line(assignment, state, current_step),
|
|
136
|
+
compact_last_done_line(state)
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
preview_heading, preview_steps = compact_preview(state)
|
|
140
|
+
unless preview_steps.empty?
|
|
141
|
+
lines << preview_heading
|
|
142
|
+
preview_steps.each do |step|
|
|
143
|
+
lines << preview_step_line(step)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
lines << compact_steps_summary_line(state)
|
|
148
|
+
lines
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def progress_summary_line(assignment, state, current_step)
|
|
152
|
+
state_label = STATE_LABELS[state.assignment_state] || state.assignment_state.to_s
|
|
153
|
+
details = ["State: #{state_label}", "Progress: #{state.done.size}/#{state.size} done"]
|
|
154
|
+
|
|
155
|
+
if current_step
|
|
156
|
+
details << "Current: #{current_step.number} #{current_step.name}"
|
|
157
|
+
elsif state.complete?
|
|
158
|
+
details << "Current: complete"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if state.last_done
|
|
162
|
+
details << "Last: #{state.last_done.number} #{state.last_done.name}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
details.join(" | ")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def compact_assignment_line(assignment, state, current_step)
|
|
169
|
+
state_label = STATE_LABELS[state.assignment_state] || state.assignment_state.to_s
|
|
170
|
+
details = ["Assignment: #{assignment.id} #{compact_assignment_name(assignment.name)}", "Status: #{state_label}"]
|
|
171
|
+
|
|
172
|
+
if current_step
|
|
173
|
+
details << "Current: #{current_step.number} #{current_step.name}"
|
|
174
|
+
end
|
|
175
|
+
details.join(" | ")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def compact_last_done_line(state)
|
|
179
|
+
return "Last done: none" unless state.last_done
|
|
180
|
+
|
|
181
|
+
"Last done: #{state.last_done.number} #{state.last_done.name}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def compact_steps_summary_line(state)
|
|
185
|
+
summary = state.summary
|
|
186
|
+
"Steps: #{progress_bar(state.done.size, state.size)} #{state.done.size}/#{state.size} done | Pending: #{summary[:pending]} | Failed: #{summary[:failed]}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def compact_assignment_name(name)
|
|
190
|
+
File.basename(name.to_s, File.extname(name.to_s))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def compact_preview(state)
|
|
194
|
+
active_or_pending = state.steps.select { |step| %i[in_progress pending].include?(step.status) }.first(PREVIEW_LIMIT)
|
|
195
|
+
return ["Pending steps:", active_or_pending] unless active_or_pending.empty?
|
|
196
|
+
|
|
197
|
+
failed_preview = state.failed.first(PREVIEW_LIMIT)
|
|
198
|
+
return ["Failed steps:", failed_preview] if state.assignment_state == :failed && failed_preview.any?
|
|
199
|
+
|
|
200
|
+
[nil, []]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def preview_step_line(step)
|
|
204
|
+
status = case step.status
|
|
205
|
+
when :in_progress then "active"
|
|
206
|
+
when :pending then "next"
|
|
207
|
+
when :failed then "failed"
|
|
208
|
+
else step.status.to_s
|
|
209
|
+
end
|
|
210
|
+
"#{step.number} #{status} #{step.name}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def progress_bar(done, total)
|
|
214
|
+
return "░" * PROGRESS_BAR_WIDTH if total <= 0
|
|
215
|
+
|
|
216
|
+
filled = ((done.to_f / total) * PROGRESS_BAR_WIDTH).round
|
|
217
|
+
filled = [[filled, 0].max, PROGRESS_BAR_WIDTH].min
|
|
218
|
+
("█" * filled) + ("░" * (PROGRESS_BAR_WIDTH - filled))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def compact_other_assignments_line(current_assignment_id, include_completed:)
|
|
222
|
+
discoverer = Molecules::AssignmentDiscoverer.new
|
|
223
|
+
others = discoverer.find_all(include_completed: include_completed).reject { |info| info.id == current_assignment_id }
|
|
224
|
+
return nil if others.empty?
|
|
225
|
+
|
|
226
|
+
active = others.count { |info| %i[running stalled].include?(info.state) }
|
|
227
|
+
pending = others.count { |info| info.state == :paused }
|
|
228
|
+
failed = others.count { |info| info.state == :failed }
|
|
229
|
+
"other assignments: #{others.size} total | active: #{active} paused: #{pending} failed: #{failed}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def print_full_status(view, target, flat:, include_completed:)
|
|
233
|
+
print_queue_status(view.assignment, view.scoped_state, flat: flat, root_number: view.scope_root)
|
|
234
|
+
|
|
235
|
+
if view.current_step
|
|
236
|
+
scoped_fork_step = scoped_fork_metadata_step(view.state, view.current_step, target.scope, view.scope_root)
|
|
237
|
+
|
|
238
|
+
puts
|
|
239
|
+
puts "Current Step: #{view.current_step.number} - #{view.current_step.name}"
|
|
240
|
+
puts "Current Status: #{view.current_step.status}"
|
|
241
|
+
print_stall_details(view.current_step)
|
|
242
|
+
puts "Workflow: #{view.current_step.workflow}" if view.current_step.workflow
|
|
243
|
+
puts "Skill: #{view.current_step.skill}" if !view.current_step.workflow && view.current_step.skill
|
|
244
|
+
puts "Context: #{view.current_step.context}" if view.current_step.context
|
|
245
|
+
|
|
246
|
+
effective_fork_provider = effective_fork_provider_for(view.current_step, scoped_fork_step)
|
|
247
|
+
puts "Fork Provider: #{effective_fork_provider}" if effective_fork_provider
|
|
248
|
+
print_scoped_fork_pid_info(scoped_fork_step)
|
|
249
|
+
elsif view.scoped_state.complete?
|
|
250
|
+
puts
|
|
251
|
+
puts "Assignment completed!"
|
|
252
|
+
end
|
|
172
253
|
|
|
173
|
-
|
|
174
|
-
|
|
254
|
+
print_other_assignments_table(view.assignment.id, include_completed: include_completed) unless target.assignment_id
|
|
255
|
+
end
|
|
175
256
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
current = scoped_state.current || scoped_state.next_workable
|
|
257
|
+
def print_stall_details(step)
|
|
258
|
+
return unless step.stall_reason
|
|
179
259
|
|
|
180
|
-
|
|
260
|
+
lines = step.stall_reason.to_s.strip.lines
|
|
261
|
+
puts "Stall Reason: #{lines.first&.chomp}"
|
|
262
|
+
lines[1..].each { |line| puts " #{line.chomp}" } if lines.length > 1
|
|
263
|
+
print_hitl_stall_guidance(lines.first.to_s)
|
|
181
264
|
end
|
|
182
265
|
|
|
183
266
|
def print_queue_status(assignment, state, flat: false, root_number: nil)
|
|
@@ -192,43 +275,31 @@ module Ace
|
|
|
192
275
|
end
|
|
193
276
|
|
|
194
277
|
def has_nested_steps?(state)
|
|
195
|
-
state.steps.any? { |
|
|
278
|
+
state.steps.any? { |step| !Atoms::StepNumbering.top_level?(step.number) }
|
|
196
279
|
end
|
|
197
280
|
|
|
198
281
|
def print_flat_status(state)
|
|
199
|
-
|
|
200
|
-
file_width = [30, state.steps.map { |s| File.basename(s.file_path || "").length }.max || 20].max
|
|
282
|
+
file_width = [30, state.steps.map { |step| File.basename(step.file_path || "").length }.max || 20].max
|
|
201
283
|
status_width = 12
|
|
202
284
|
name_width = 20
|
|
203
285
|
|
|
204
|
-
# Header
|
|
205
286
|
puts format("%-#{file_width}s %-#{status_width}s %-#{name_width}s", "FILE", "STATUS", "NAME")
|
|
206
287
|
|
|
207
|
-
# Rows
|
|
208
288
|
state.steps.each do |step|
|
|
209
289
|
file = File.basename(step.file_path || "#{step.number}-#{step.name}.st.md")
|
|
210
290
|
status = format_status(step.status)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
row = format("%-#{file_width}s %-#{status_width}s %-#{name_width}s", file, status, name)
|
|
214
|
-
|
|
215
|
-
# Add error message for failed steps
|
|
216
|
-
if step.status == :failed && step.error
|
|
217
|
-
row += " (#{step.error})"
|
|
218
|
-
end
|
|
219
|
-
|
|
291
|
+
row = format("%-#{file_width}s %-#{status_width}s %-#{name_width}s", file, status, step.name)
|
|
292
|
+
row += " (#{step.error})" if step.status == :failed && step.error
|
|
220
293
|
puts row
|
|
221
294
|
end
|
|
222
295
|
end
|
|
223
296
|
|
|
224
297
|
def print_hierarchical_status(state, root_number: nil)
|
|
225
|
-
# Header
|
|
226
298
|
puts format("%-#{COL_NUMBER}s %-#{COL_STATUS}s %-#{COL_NAME}s %-#{COL_FORK}s %s", "NUMBER", "STATUS", "NAME", "FORK", "CHILDREN")
|
|
227
299
|
puts "-" * 78
|
|
228
300
|
|
|
229
|
-
# Print hierarchy with tree structure
|
|
230
301
|
nodes = root_hierarchy_nodes(state, root_number)
|
|
231
|
-
print_hierarchy_level(nodes,
|
|
302
|
+
print_hierarchy_level(nodes, depth: 0)
|
|
232
303
|
end
|
|
233
304
|
|
|
234
305
|
def root_hierarchy_nodes(state, root_number)
|
|
@@ -241,63 +312,34 @@ module Ace
|
|
|
241
312
|
end
|
|
242
313
|
|
|
243
314
|
def build_hierarchy_node(state, step)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
{step: step, children: children}
|
|
315
|
+
{
|
|
316
|
+
step: step,
|
|
317
|
+
children: state.children_of(step.number).map { |child| build_hierarchy_node(state, child) }
|
|
318
|
+
}
|
|
249
319
|
end
|
|
250
320
|
|
|
251
|
-
def print_hierarchy_level(nodes,
|
|
321
|
+
def print_hierarchy_level(nodes, depth:)
|
|
252
322
|
nodes.each_with_index do |node, index|
|
|
253
323
|
step = node[:step]
|
|
254
324
|
children = node[:children]
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
# Build tree prefix
|
|
258
|
-
prefix = if depth == 0
|
|
325
|
+
prefix = if depth.zero?
|
|
259
326
|
""
|
|
260
327
|
else
|
|
261
|
-
|
|
262
|
-
connector = is_last ? "\\-- " : "|-- "
|
|
263
|
-
indent + connector
|
|
328
|
+
(" " * (depth - 1)) + (index == nodes.size - 1 ? "\\-- " : "|-- ")
|
|
264
329
|
end
|
|
265
330
|
|
|
266
|
-
# Format number with hierarchy indicator
|
|
267
|
-
number_display = prefix + step.number
|
|
268
|
-
|
|
269
|
-
# Status with icon
|
|
270
331
|
status_icon = STATUS_ICONS[step.status] || step.status.to_s.capitalize
|
|
271
|
-
|
|
272
|
-
# Fork indicator reflects execution context, not child presence.
|
|
273
332
|
fork_info = step.fork? ? "yes" : ""
|
|
274
|
-
|
|
275
|
-
# Children count (progress visibility)
|
|
276
|
-
child_info = if children.any?
|
|
277
|
-
incomplete = children.count { |c| c[:step].status != :done }
|
|
278
|
-
if incomplete > 0
|
|
279
|
-
"(#{children.size - incomplete}/#{children.size} done)"
|
|
280
|
-
else
|
|
281
|
-
"(#{children.size}/#{children.size} done)"
|
|
282
|
-
end
|
|
283
|
-
else
|
|
284
|
-
""
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Error info for failed steps
|
|
333
|
+
child_info = children.any? ? "(#{children.count { |c| c[:step].status == :done }}/#{children.size} done)" : ""
|
|
288
334
|
error_suffix = (step.status == :failed && step.error) ? " - #{step.error}" : ""
|
|
335
|
+
display_name = step.name.length > COL_NAME ? "#{step.name[0..COL_NAME - 4]}..." : step.name
|
|
289
336
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
step.
|
|
293
|
-
|
|
294
|
-
step.name
|
|
295
|
-
end
|
|
296
|
-
puts format("%-#{COL_NUMBER}s %-#{COL_STATUS}s %-#{COL_NAME}s %-#{COL_FORK}s %s%s",
|
|
297
|
-
number_display, status_icon, display_name, fork_info, child_info, error_suffix)
|
|
337
|
+
puts format(
|
|
338
|
+
"%-#{COL_NUMBER}s %-#{COL_STATUS}s %-#{COL_NAME}s %-#{COL_FORK}s %s%s",
|
|
339
|
+
prefix + step.number, status_icon, display_name, fork_info, child_info, error_suffix
|
|
340
|
+
)
|
|
298
341
|
|
|
299
|
-
|
|
300
|
-
print_hierarchy_level(children, state, depth: depth + 1) if children.any?
|
|
342
|
+
print_hierarchy_level(children, depth: depth + 1) if children.any?
|
|
301
343
|
end
|
|
302
344
|
end
|
|
303
345
|
|
|
@@ -305,57 +347,6 @@ module Ace
|
|
|
305
347
|
STATUS_ICONS[status]&.split(" ")&.last || status.to_s.capitalize
|
|
306
348
|
end
|
|
307
349
|
|
|
308
|
-
# Print Task tool instructions for a fork context step
|
|
309
|
-
def print_fork_instructions(step, assignment)
|
|
310
|
-
escaped_name = step.name.gsub('"', '\\"')
|
|
311
|
-
# Derive project root from cache_dir: /project/.ace-local/assign/assignment-id -> /project
|
|
312
|
-
project_root = assignment.cache_dir ? File.expand_path("../../..", assignment.cache_dir) : Dir.pwd
|
|
313
|
-
|
|
314
|
-
puts "Execute this step in a forked context:"
|
|
315
|
-
puts
|
|
316
|
-
puts " Task tool parameters:"
|
|
317
|
-
puts " description: \"#{escaped_name}\""
|
|
318
|
-
puts " prompt: (see below)"
|
|
319
|
-
puts
|
|
320
|
-
puts " Prompt for forked agent:"
|
|
321
|
-
puts " ========================"
|
|
322
|
-
puts step.instructions
|
|
323
|
-
puts " ========================"
|
|
324
|
-
puts
|
|
325
|
-
puts " Working directory: #{project_root}"
|
|
326
|
-
puts " Assignment: #{assignment.id}"
|
|
327
|
-
puts
|
|
328
|
-
puts "After completing, run:"
|
|
329
|
-
puts " ace-assign finish --message <report-file.md>"
|
|
330
|
-
puts
|
|
331
|
-
puts "To execute entire subtree in one forked process:"
|
|
332
|
-
puts " ace-assign fork-run --root #{step.number} --assignment #{assignment.id}"
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def fork_scope_root(state, current_step)
|
|
336
|
-
return nil unless current_step
|
|
337
|
-
return current_step if current_step.fork?
|
|
338
|
-
|
|
339
|
-
state.nearest_fork_ancestor(current_step.number)
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
def scoped_fork_metadata_step(state, current_step, scope, scope_root)
|
|
343
|
-
return nil unless current_step
|
|
344
|
-
|
|
345
|
-
if scope && !scope.strip.empty?
|
|
346
|
-
return state.find_by_number(scope_root || scope.strip)
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
fork_scope_root(state, current_step)
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
def effective_fork_provider_for(current_step, scoped_fork_step)
|
|
353
|
-
return nil unless current_step
|
|
354
|
-
|
|
355
|
-
provider = current_step.fork_provider || scoped_fork_step&.fork_provider
|
|
356
|
-
provider.to_s.strip.empty? ? nil : provider
|
|
357
|
-
end
|
|
358
|
-
|
|
359
350
|
def print_scoped_fork_pid_info(step)
|
|
360
351
|
return unless step
|
|
361
352
|
|
|
@@ -365,9 +356,8 @@ module Ace
|
|
|
365
356
|
return unless has_pid || has_tree || has_file
|
|
366
357
|
|
|
367
358
|
puts "Scoped Fork PID: #{step.fork_launch_pid}" if has_pid
|
|
368
|
-
puts "Scoped Fork PID Tree: #{step.fork_tracked_pids.join(
|
|
359
|
+
puts "Scoped Fork PID Tree: #{step.fork_tracked_pids.join(', ')}" if has_tree
|
|
369
360
|
puts "Scoped Fork PID File: #{step.fork_pid_file}" if has_file
|
|
370
|
-
puts
|
|
371
361
|
end
|
|
372
362
|
|
|
373
363
|
def print_hitl_stall_guidance(first_line)
|
|
@@ -393,13 +383,9 @@ module Ace
|
|
|
393
383
|
{id: id, path: path.empty? ? nil : path}
|
|
394
384
|
end
|
|
395
385
|
|
|
396
|
-
|
|
397
|
-
def print_other_assignments(current_assignment_id, include_completed:)
|
|
386
|
+
def print_other_assignments_table(current_assignment_id, include_completed:)
|
|
398
387
|
discoverer = Molecules::AssignmentDiscoverer.new
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
# Exclude the current assignment
|
|
402
|
-
others = all_assignments.reject { |ai| ai.id == current_assignment_id }
|
|
388
|
+
others = discoverer.find_all(include_completed: include_completed).reject { |info| info.id == current_assignment_id }
|
|
403
389
|
return if others.empty?
|
|
404
390
|
|
|
405
391
|
puts
|
|
@@ -416,8 +402,7 @@ module Ace
|
|
|
416
402
|
others.each do |info|
|
|
417
403
|
state_label = STATE_LABELS[info.state] || info.state.to_s
|
|
418
404
|
updated = format_relative_time(info.updated_at)
|
|
419
|
-
step =
|
|
420
|
-
|
|
405
|
+
step = info.current_step.length > col_step ? "#{info.current_step[0..col_step - 4]}..." : info.current_step
|
|
421
406
|
puts format("%-#{col_id}s %-#{col_status}s %-#{col_progress}s %-#{col_step}s %s",
|
|
422
407
|
info.id, state_label, info.progress, step, updated)
|
|
423
408
|
end
|
|
@@ -427,15 +412,11 @@ module Ace
|
|
|
427
412
|
return "-" unless time
|
|
428
413
|
|
|
429
414
|
diff = Time.now - time
|
|
430
|
-
if diff < 60
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
"#{(diff / 3600).to_i}h ago"
|
|
436
|
-
else
|
|
437
|
-
"#{(diff / 86_400).to_i}d ago"
|
|
438
|
-
end
|
|
415
|
+
return "#{diff.to_i}s ago" if diff < 60
|
|
416
|
+
return "#{(diff / 60).to_i}m ago" if diff < 3600
|
|
417
|
+
return "#{(diff / 3600).to_i}h ago" if diff < 86_400
|
|
418
|
+
|
|
419
|
+
"#{(diff / 86_400).to_i}d ago"
|
|
439
420
|
end
|
|
440
421
|
end
|
|
441
422
|
end
|
|
@@ -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 current, 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 current, 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.current_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 current or next workable step"
|
|
56
|
+
].join("\n")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|