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
@@ -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
- in_progress: "▶ Active",
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 (no hierarchy)"
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
- executor = build_executor_for_target(target)
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
- if current_for_display
83
- fork_root = fork_scope_root(state, current_for_display)
84
- scoped_fork_step = scoped_fork_metadata_step(state, current_for_display, target.scope, scope_root)
85
-
86
- puts
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)
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
- 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
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
- # Show other assignments section (unless targeting a specific assignment)
130
- unless target.assignment_id
131
- print_other_assignments(result[:assignment].id, include_completed: options[:all])
132
- end
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 status_to_h(assignment, state, current_step, scoped_fork_step: nil)
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
- current_step: step_to_h(current_step, effective_fork_provider: effective_fork_provider_for(current_step, scoped_fork_step)),
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 scoped_status_view(state, scope)
171
- return {state: state, current: state.current, root: nil} if scope.nil? || scope.strip.empty?
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
- root = state.find_by_number(scope.strip)
174
- raise StepErrors::NotFound, "Step #{scope} not found in queue" unless root
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
- scoped_steps = state.subtree_steps(root.number)
177
- scoped_state = Models::QueueState.new(steps: scoped_steps, assignment: state.assignment)
178
- current = scoped_state.current || scoped_state.next_workable
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
- {state: scoped_state, current: current, root: root.number}
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? { |s| !Atoms::StepNumbering.top_level?(s.number) }
303
+ state.steps.any? { |step| !Atoms::StepNumbering.top_level?(step.number) }
196
304
  end
197
305
 
198
306
  def print_flat_status(state)
199
- # Calculate column widths
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
- name = step.name
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, state, depth: 0)
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
- children = state.children_of(step.number).map do |child|
245
- build_hierarchy_node(state, child)
246
- end
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, state, depth:)
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
- is_last = index == nodes.size - 1
256
-
257
- # Build tree prefix
258
- prefix = if depth == 0
350
+ prefix = if depth.zero?
259
351
  ""
260
352
  else
261
- indent = " " * (depth - 1)
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
- # Truncate name with ellipsis if too long
291
- display_name = if step.name.length > COL_NAME
292
- step.name[0..COL_NAME - 4] + "..."
293
- else
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
- # Recurse for children
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(", ")}" if has_tree
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
- # Print other assignments section
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
- all_assignments = discoverer.find_all(include_completed: include_completed)
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", "CURRENT STEP", "UPDATED")
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 = (info.current_step.length > col_step) ? info.current_step[0..col_step - 4] + "..." : info.current_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
- "#{diff.to_i}s ago"
432
- elsif diff < 3600
433
- "#{(diff / 60).to_i}m ago"
434
- elsif diff < 86_400
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