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
|
@@ -6,52 +6,38 @@ 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
|
+
active: "▶ Active",
|
|
33
17
|
pending: "○ Pending",
|
|
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,92 +46,56 @@ 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
|
|
79
|
-
|
|
80
|
-
print_queue_status(assignment, scoped_state, flat: options[:flat], root_number: scope_root)
|
|
51
|
+
return if options[:quiet]
|
|
81
52
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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)
|
|
53
|
+
if options[:format] == "json"
|
|
54
|
+
puts JSON.pretty_generate(
|
|
55
|
+
status_to_h(view.assignment, view.scoped_state, view.active_steps, view.next_step, target: target, scope_root: view.scope_root)
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
end
|
|
109
59
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
60
|
+
mode = normalize_mode(options[:mode])
|
|
61
|
+
raise Ace::Support::Cli::Error, "--flat is supported only with --mode full" if options[:flat] && mode != "full"
|
|
62
|
+
raise Ace::Support::Cli::Error, "--all is supported only with --mode full or compact" if options[:all] && mode == "progress"
|
|
128
63
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
64
|
+
case mode
|
|
65
|
+
when "progress"
|
|
66
|
+
puts progress_summary_line(view.assignment, view.scoped_state, view.active_steps, view.next_step)
|
|
67
|
+
when "full"
|
|
68
|
+
print_full_status(view, target, flat: options[:flat], include_completed: options[:all])
|
|
69
|
+
else
|
|
70
|
+
print_compact_status(view, target, include_completed: options[:all])
|
|
133
71
|
end
|
|
134
72
|
end
|
|
135
73
|
|
|
136
74
|
private
|
|
137
75
|
|
|
138
|
-
def
|
|
139
|
-
|
|
76
|
+
def normalize_mode(value)
|
|
77
|
+
mode = value.to_s.strip
|
|
78
|
+
mode = "compact" if mode.empty?
|
|
79
|
+
allowed = %w[compact progress full]
|
|
80
|
+
raise Ace::Support::Cli::Error, "Unsupported status mode '#{mode}'. Use one of: #{allowed.join(', ')}." unless allowed.include?(mode)
|
|
81
|
+
|
|
82
|
+
mode
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def status_to_h(assignment, state, active_steps, next_step, target:, scope_root:)
|
|
86
|
+
payload = {
|
|
140
87
|
assignment: {
|
|
141
88
|
id: assignment.id,
|
|
142
89
|
name: assignment.name,
|
|
143
90
|
state: state.assignment_state.to_s
|
|
144
91
|
},
|
|
145
92
|
steps: state.steps.map { |step| step_to_h(step) },
|
|
146
|
-
|
|
93
|
+
active_steps: active_steps.map { |step| step_to_h(step, effective_fork_provider: effective_fork_provider_for(step, scoped_fork_metadata_step(state, step, target.scope, scope_root))) },
|
|
147
94
|
progress: "#{state.done.size}/#{state.size} done"
|
|
148
95
|
}
|
|
96
|
+
|
|
97
|
+
payload[:next_step] = step_to_h(next_step, effective_fork_provider: effective_fork_provider_for(next_step, scoped_fork_metadata_step(state, next_step, target.scope, scope_root))) if active_steps.empty? && next_step
|
|
98
|
+
payload
|
|
149
99
|
end
|
|
150
100
|
|
|
151
101
|
def step_to_h(step, effective_fork_provider: nil)
|
|
@@ -167,17 +117,175 @@ module Ace
|
|
|
167
117
|
}.compact
|
|
168
118
|
end
|
|
169
119
|
|
|
170
|
-
def
|
|
171
|
-
|
|
120
|
+
def print_compact_status(view, target, include_completed:)
|
|
121
|
+
lines = []
|
|
122
|
+
lines.concat(compact_summary_lines(view.assignment, view.scoped_state, view.active_steps, view.next_step))
|
|
123
|
+
|
|
124
|
+
unless target.assignment_id
|
|
125
|
+
other_line = compact_other_assignments_line(view.assignment.id, include_completed: include_completed)
|
|
126
|
+
lines << other_line if other_line
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
puts lines.take(10).join("\n")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def compact_summary_lines(assignment, state, active_steps, next_step)
|
|
133
|
+
lines = [
|
|
134
|
+
compact_assignment_line(assignment, state, active_steps, next_step),
|
|
135
|
+
compact_last_done_line(state)
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
preview_heading, preview_steps = compact_preview(state)
|
|
139
|
+
unless preview_steps.empty?
|
|
140
|
+
lines << preview_heading
|
|
141
|
+
preview_steps.each do |step|
|
|
142
|
+
lines << preview_step_line(step)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
lines << compact_steps_summary_line(state)
|
|
147
|
+
lines
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def progress_summary_line(assignment, state, active_steps, next_step)
|
|
151
|
+
state_label = STATE_LABELS[state.assignment_state] || state.assignment_state.to_s
|
|
152
|
+
details = ["State: #{state_label}", "Progress: #{state.done.size}/#{state.size} done"]
|
|
153
|
+
|
|
154
|
+
if active_steps.any?
|
|
155
|
+
details << "Active: #{step_refs(active_steps)}"
|
|
156
|
+
elsif next_step
|
|
157
|
+
details << "Next: #{next_step.number} #{next_step.name}"
|
|
158
|
+
elsif state.complete?
|
|
159
|
+
details << "Next: complete"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
if state.last_done
|
|
163
|
+
details << "Last: #{state.last_done.number} #{state.last_done.name}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
details.join(" | ")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def compact_assignment_line(assignment, state, active_steps, next_step)
|
|
170
|
+
state_label = STATE_LABELS[state.assignment_state] || state.assignment_state.to_s
|
|
171
|
+
details = ["Assignment: #{assignment.id} #{compact_assignment_name(assignment.name)}", "Status: #{state_label}"]
|
|
172
|
+
|
|
173
|
+
if active_steps.any?
|
|
174
|
+
details << "Active: #{step_refs(active_steps)}"
|
|
175
|
+
elsif next_step
|
|
176
|
+
details << "Next: #{next_step.number} #{next_step.name}"
|
|
177
|
+
end
|
|
178
|
+
details.join(" | ")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def compact_last_done_line(state)
|
|
182
|
+
return "Last done: none" unless state.last_done
|
|
183
|
+
|
|
184
|
+
"Last done: #{state.last_done.number} #{state.last_done.name}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def compact_steps_summary_line(state)
|
|
188
|
+
summary = state.summary
|
|
189
|
+
"Steps: #{progress_bar(state.done.size, state.size)} #{state.done.size}/#{state.size} done | Pending: #{summary[:pending]} | Failed: #{summary[:failed]}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def compact_assignment_name(name)
|
|
193
|
+
File.basename(name.to_s, File.extname(name.to_s))
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def compact_preview(state)
|
|
197
|
+
active_or_pending = state.steps.select { |step| %i[active pending].include?(step.status) }.first(PREVIEW_LIMIT)
|
|
198
|
+
return ["Pending steps:", active_or_pending] unless active_or_pending.empty?
|
|
199
|
+
|
|
200
|
+
failed_preview = state.failed.first(PREVIEW_LIMIT)
|
|
201
|
+
return ["Failed steps:", failed_preview] if state.assignment_state == :failed && failed_preview.any?
|
|
202
|
+
|
|
203
|
+
[nil, []]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def preview_step_line(step)
|
|
207
|
+
status = case step.status
|
|
208
|
+
when :active then "active"
|
|
209
|
+
when :pending then "next"
|
|
210
|
+
when :failed then "failed"
|
|
211
|
+
else step.status.to_s
|
|
212
|
+
end
|
|
213
|
+
"#{step.number} #{status} #{step.name}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def progress_bar(done, total)
|
|
217
|
+
return "░" * PROGRESS_BAR_WIDTH if total <= 0
|
|
218
|
+
|
|
219
|
+
filled = ((done.to_f / total) * PROGRESS_BAR_WIDTH).round
|
|
220
|
+
filled = [[filled, 0].max, PROGRESS_BAR_WIDTH].min
|
|
221
|
+
("█" * filled) + ("░" * (PROGRESS_BAR_WIDTH - filled))
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def compact_other_assignments_line(current_assignment_id, include_completed:)
|
|
225
|
+
discoverer = Molecules::AssignmentDiscoverer.new
|
|
226
|
+
others = discoverer.find_all(include_completed: include_completed).reject { |info| info.id == current_assignment_id }
|
|
227
|
+
return nil if others.empty?
|
|
228
|
+
|
|
229
|
+
active = others.count { |info| %i[running stalled].include?(info.state) }
|
|
230
|
+
pending = others.count { |info| info.state == :paused }
|
|
231
|
+
failed = others.count { |info| info.state == :failed }
|
|
232
|
+
"other assignments: #{others.size} total | active: #{active} paused: #{pending} failed: #{failed}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def step_refs(steps, limit: 3)
|
|
236
|
+
refs = steps.first(limit).map { |step| "#{step.number} #{step.name}" }
|
|
237
|
+
refs << "+#{steps.size - limit} more" if steps.size > limit
|
|
238
|
+
refs.join(" | ")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def print_full_status(view, target, flat:, include_completed:)
|
|
242
|
+
print_queue_status(view.assignment, view.scoped_state, flat: flat, root_number: view.scope_root)
|
|
243
|
+
|
|
244
|
+
if view.active_steps.any?
|
|
245
|
+
puts
|
|
246
|
+
puts "Active Steps:"
|
|
247
|
+
view.active_steps.each do |step|
|
|
248
|
+
puts " #{step.number} - #{step.name}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
if view.focus_step
|
|
252
|
+
scoped_fork_step = scoped_fork_metadata_step(view.state, view.focus_step, target.scope, view.scope_root)
|
|
172
253
|
|
|
173
|
-
|
|
174
|
-
|
|
254
|
+
puts
|
|
255
|
+
puts "Focused Step: #{view.focus_step.number} - #{view.focus_step.name}"
|
|
256
|
+
puts "Focused Status: #{view.focus_step.status}"
|
|
257
|
+
print_stall_details(view.focus_step)
|
|
258
|
+
puts "Workflow: #{view.focus_step.workflow}" if view.focus_step.workflow
|
|
259
|
+
puts "Skill: #{view.focus_step.skill}" if !view.focus_step.workflow && view.focus_step.skill
|
|
260
|
+
puts "Context: #{view.focus_step.context}" if view.focus_step.context
|
|
261
|
+
|
|
262
|
+
effective_fork_provider = effective_fork_provider_for(view.focus_step, scoped_fork_step)
|
|
263
|
+
puts "Fork Provider: #{effective_fork_provider}" if effective_fork_provider
|
|
264
|
+
print_scoped_fork_pid_info(scoped_fork_step)
|
|
265
|
+
end
|
|
266
|
+
elsif view.next_step
|
|
267
|
+
scoped_fork_step = scoped_fork_metadata_step(view.state, view.next_step, target.scope, view.scope_root)
|
|
268
|
+
|
|
269
|
+
puts
|
|
270
|
+
puts "Next Step: #{view.next_step.number} - #{view.next_step.name}"
|
|
271
|
+
effective_fork_provider = effective_fork_provider_for(view.next_step, scoped_fork_step)
|
|
272
|
+
puts "Fork Provider: #{effective_fork_provider}" if effective_fork_provider
|
|
273
|
+
print_scoped_fork_pid_info(scoped_fork_step)
|
|
274
|
+
elsif view.scoped_state.complete?
|
|
275
|
+
puts
|
|
276
|
+
puts "Assignment completed!"
|
|
277
|
+
end
|
|
175
278
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
279
|
+
print_other_assignments_table(view.assignment.id, include_completed: include_completed) unless target.assignment_id
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def print_stall_details(step)
|
|
283
|
+
return unless step.stall_reason
|
|
179
284
|
|
|
180
|
-
|
|
285
|
+
lines = step.stall_reason.to_s.strip.lines
|
|
286
|
+
puts "Stall Reason: #{lines.first&.chomp}"
|
|
287
|
+
lines[1..].each { |line| puts " #{line.chomp}" } if lines.length > 1
|
|
288
|
+
print_hitl_stall_guidance(lines.first.to_s)
|
|
181
289
|
end
|
|
182
290
|
|
|
183
291
|
def print_queue_status(assignment, state, flat: false, root_number: nil)
|
|
@@ -192,43 +300,31 @@ module Ace
|
|
|
192
300
|
end
|
|
193
301
|
|
|
194
302
|
def has_nested_steps?(state)
|
|
195
|
-
state.steps.any? { |
|
|
303
|
+
state.steps.any? { |step| !Atoms::StepNumbering.top_level?(step.number) }
|
|
196
304
|
end
|
|
197
305
|
|
|
198
306
|
def print_flat_status(state)
|
|
199
|
-
|
|
200
|
-
file_width = [30, state.steps.map { |s| File.basename(s.file_path || "").length }.max || 20].max
|
|
307
|
+
file_width = [30, state.steps.map { |step| File.basename(step.file_path || "").length }.max || 20].max
|
|
201
308
|
status_width = 12
|
|
202
309
|
name_width = 20
|
|
203
310
|
|
|
204
|
-
# Header
|
|
205
311
|
puts format("%-#{file_width}s %-#{status_width}s %-#{name_width}s", "FILE", "STATUS", "NAME")
|
|
206
312
|
|
|
207
|
-
# Rows
|
|
208
313
|
state.steps.each do |step|
|
|
209
314
|
file = File.basename(step.file_path || "#{step.number}-#{step.name}.st.md")
|
|
210
315
|
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
|
-
|
|
316
|
+
row = format("%-#{file_width}s %-#{status_width}s %-#{name_width}s", file, status, step.name)
|
|
317
|
+
row += " (#{step.error})" if step.status == :failed && step.error
|
|
220
318
|
puts row
|
|
221
319
|
end
|
|
222
320
|
end
|
|
223
321
|
|
|
224
322
|
def print_hierarchical_status(state, root_number: nil)
|
|
225
|
-
# Header
|
|
226
323
|
puts format("%-#{COL_NUMBER}s %-#{COL_STATUS}s %-#{COL_NAME}s %-#{COL_FORK}s %s", "NUMBER", "STATUS", "NAME", "FORK", "CHILDREN")
|
|
227
324
|
puts "-" * 78
|
|
228
325
|
|
|
229
|
-
# Print hierarchy with tree structure
|
|
230
326
|
nodes = root_hierarchy_nodes(state, root_number)
|
|
231
|
-
print_hierarchy_level(nodes,
|
|
327
|
+
print_hierarchy_level(nodes, depth: 0)
|
|
232
328
|
end
|
|
233
329
|
|
|
234
330
|
def root_hierarchy_nodes(state, root_number)
|
|
@@ -241,63 +337,34 @@ module Ace
|
|
|
241
337
|
end
|
|
242
338
|
|
|
243
339
|
def build_hierarchy_node(state, step)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
{step: step, children: children}
|
|
340
|
+
{
|
|
341
|
+
step: step,
|
|
342
|
+
children: state.children_of(step.number).map { |child| build_hierarchy_node(state, child) }
|
|
343
|
+
}
|
|
249
344
|
end
|
|
250
345
|
|
|
251
|
-
def print_hierarchy_level(nodes,
|
|
346
|
+
def print_hierarchy_level(nodes, depth:)
|
|
252
347
|
nodes.each_with_index do |node, index|
|
|
253
348
|
step = node[:step]
|
|
254
349
|
children = node[:children]
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
# Build tree prefix
|
|
258
|
-
prefix = if depth == 0
|
|
350
|
+
prefix = if depth.zero?
|
|
259
351
|
""
|
|
260
352
|
else
|
|
261
|
-
|
|
262
|
-
connector = is_last ? "\\-- " : "|-- "
|
|
263
|
-
indent + connector
|
|
353
|
+
(" " * (depth - 1)) + (index == nodes.size - 1 ? "\\-- " : "|-- ")
|
|
264
354
|
end
|
|
265
355
|
|
|
266
|
-
# Format number with hierarchy indicator
|
|
267
|
-
number_display = prefix + step.number
|
|
268
|
-
|
|
269
|
-
# Status with icon
|
|
270
356
|
status_icon = STATUS_ICONS[step.status] || step.status.to_s.capitalize
|
|
271
|
-
|
|
272
|
-
# Fork indicator reflects execution context, not child presence.
|
|
273
357
|
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
|
|
358
|
+
child_info = children.any? ? "(#{children.count { |c| c[:step].status == :done }}/#{children.size} done)" : ""
|
|
288
359
|
error_suffix = (step.status == :failed && step.error) ? " - #{step.error}" : ""
|
|
360
|
+
display_name = step.name.length > COL_NAME ? "#{step.name[0..COL_NAME - 4]}..." : step.name
|
|
289
361
|
|
|
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)
|
|
362
|
+
puts format(
|
|
363
|
+
"%-#{COL_NUMBER}s %-#{COL_STATUS}s %-#{COL_NAME}s %-#{COL_FORK}s %s%s",
|
|
364
|
+
prefix + step.number, status_icon, display_name, fork_info, child_info, error_suffix
|
|
365
|
+
)
|
|
298
366
|
|
|
299
|
-
|
|
300
|
-
print_hierarchy_level(children, state, depth: depth + 1) if children.any?
|
|
367
|
+
print_hierarchy_level(children, depth: depth + 1) if children.any?
|
|
301
368
|
end
|
|
302
369
|
end
|
|
303
370
|
|
|
@@ -305,57 +372,6 @@ module Ace
|
|
|
305
372
|
STATUS_ICONS[status]&.split(" ")&.last || status.to_s.capitalize
|
|
306
373
|
end
|
|
307
374
|
|
|
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
375
|
def print_scoped_fork_pid_info(step)
|
|
360
376
|
return unless step
|
|
361
377
|
|
|
@@ -365,9 +381,8 @@ module Ace
|
|
|
365
381
|
return unless has_pid || has_tree || has_file
|
|
366
382
|
|
|
367
383
|
puts "Scoped Fork PID: #{step.fork_launch_pid}" if has_pid
|
|
368
|
-
puts "Scoped Fork PID Tree: #{step.fork_tracked_pids.join(
|
|
384
|
+
puts "Scoped Fork PID Tree: #{step.fork_tracked_pids.join(', ')}" if has_tree
|
|
369
385
|
puts "Scoped Fork PID File: #{step.fork_pid_file}" if has_file
|
|
370
|
-
puts
|
|
371
386
|
end
|
|
372
387
|
|
|
373
388
|
def print_hitl_stall_guidance(first_line)
|
|
@@ -393,13 +408,9 @@ module Ace
|
|
|
393
408
|
{id: id, path: path.empty? ? nil : path}
|
|
394
409
|
end
|
|
395
410
|
|
|
396
|
-
|
|
397
|
-
def print_other_assignments(current_assignment_id, include_completed:)
|
|
411
|
+
def print_other_assignments_table(current_assignment_id, include_completed:)
|
|
398
412
|
discoverer = Molecules::AssignmentDiscoverer.new
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
# Exclude the current assignment
|
|
402
|
-
others = all_assignments.reject { |ai| ai.id == current_assignment_id }
|
|
413
|
+
others = discoverer.find_all(include_completed: include_completed).reject { |info| info.id == current_assignment_id }
|
|
403
414
|
return if others.empty?
|
|
404
415
|
|
|
405
416
|
puts
|
|
@@ -411,13 +422,13 @@ module Ace
|
|
|
411
422
|
col_progress = 10
|
|
412
423
|
col_step = 20
|
|
413
424
|
puts format("%-#{col_id}s %-#{col_status}s %-#{col_progress}s %-#{col_step}s %s",
|
|
414
|
-
"ASSIGNMENT", "STATUS", "PROGRESS", "
|
|
425
|
+
"ASSIGNMENT", "STATUS", "PROGRESS", "ACTIVE/NEXT", "UPDATED")
|
|
415
426
|
|
|
416
427
|
others.each do |info|
|
|
417
428
|
state_label = STATE_LABELS[info.state] || info.state.to_s
|
|
418
429
|
updated = format_relative_time(info.updated_at)
|
|
419
|
-
step =
|
|
420
|
-
|
|
430
|
+
step = info.step_focus
|
|
431
|
+
step = step.length > col_step ? "#{step[0..col_step - 4]}..." : step
|
|
421
432
|
puts format("%-#{col_id}s %-#{col_status}s %-#{col_progress}s %-#{col_step}s %s",
|
|
422
433
|
info.id, state_label, info.progress, step, updated)
|
|
423
434
|
end
|
|
@@ -427,15 +438,11 @@ module Ace
|
|
|
427
438
|
return "-" unless time
|
|
428
439
|
|
|
429
440
|
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
|
|
441
|
+
return "#{diff.to_i}s ago" if diff < 60
|
|
442
|
+
return "#{(diff / 60).to_i}m ago" if diff < 3600
|
|
443
|
+
return "#{(diff / 3600).to_i}h ago" if diff < 86_400
|
|
444
|
+
|
|
445
|
+
"#{(diff / 86_400).to_i}d ago"
|
|
439
446
|
end
|
|
440
447
|
end
|
|
441
448
|
end
|