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
@@ -21,6 +21,8 @@ module Ace
21
21
  option :provider, desc: "LLM provider:model override (e.g., codex:gpt-5, claude:sonnet)"
22
22
  option :cli_args, desc: "Extra CLI args for provider process"
23
23
  option :timeout, type: :integer, desc: "Execution timeout in seconds"
24
+ option :launch_mode, desc: "Launch mode: auto, headless, or tmux"
25
+ option :callback, type: :boolean, default: false, desc: "Capture the origin tmux pane and let the forked agent send a final callback message there"
24
26
  option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
25
27
  option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
26
28
 
@@ -40,35 +42,35 @@ module Ace
40
42
  root_step = resolve_root_step(state, current, options[:root], target.scope)
41
43
  ensure_root_is_fork!(root_step)
42
44
  resolved_provider = resolved_provider_for(root_step, options[:provider])
45
+ resolved_launch_mode = resolved_launch_mode_for(root_step, options[:launch_mode])
46
+ callback_pane = resolve_callback_pane(options[:callback], resolved_launch_mode)
43
47
 
44
48
  if state.subtree_complete?(root_step.number)
45
49
  puts "Subtree #{root_step.number} is already complete." unless options[:quiet]
46
50
  return
47
51
  end
48
52
 
53
+ ensure_not_same_scoped_refork!(assignment_id: assignment.id, fork_root: root_step.number)
54
+
49
55
  unless options[:quiet]
50
56
  next_step = state.next_workable_in_subtree(root_step.number)
51
57
  puts "Starting fork subtree execution: #{root_step.number} - #{root_step.name}"
52
58
  puts "Assignment: #{assignment.id}"
53
59
  puts "Provider: #{resolved_provider}"
60
+ puts "Launch mode: #{resolved_launch_mode}"
61
+ puts "Callback pane: #{callback_pane}" if callback_pane
54
62
  puts "Timeout: #{options[:timeout] || Ace::Assign.config.dig("execution", "timeout") || Molecules::ForkSessionLauncher::DEFAULT_TIMEOUT}s"
55
63
  puts "Next step: #{next_step.number} - #{next_step.name}" if next_step
56
64
  end
57
65
 
58
- active_in_subtree = state.in_progress_in_subtree(root_step.number)
59
- if active_in_subtree.size > 1
60
- active_refs = active_in_subtree.map { |step| "#{step.number}(#{step.name})" }.join(", ")
61
- raise StepErrors::InvalidState, "Cannot fork-run subtree #{root_step.number}: multiple steps are already in progress (#{active_refs})."
66
+ if state.active_branch_conflict_in_subtree?(root_step.number)
67
+ active_refs = state.active_in_subtree(root_step.number).map { |step| "#{step.number}(#{step.name})" }.join(", ")
68
+ raise StepErrors::InvalidState, "Cannot fork-run subtree #{root_step.number}: multiple active branches already exist (#{active_refs})."
62
69
  end
63
70
 
64
- # Mark the next workable step as in_progress only when no subtree step is active.
65
- # For leaf fork roots, this activates the root itself.
66
- if active_in_subtree.empty?
67
- first_workable = state.next_workable_in_subtree(root_step.number)
68
- if first_workable
69
- step_writer = Molecules::StepWriter.new
70
- step_writer.mark_in_progress(first_workable.file_path)
71
- end
71
+ if root_step.status == :pending
72
+ step_writer = Molecules::StepWriter.new
73
+ step_writer.mark_active(root_step.file_path)
72
74
  end
73
75
 
74
76
  launch_result = launcher.launch(
@@ -77,10 +79,17 @@ module Ace
77
79
  provider: resolved_provider,
78
80
  cli_args: options[:cli_args],
79
81
  timeout: options[:timeout],
80
- cache_dir: assignment.cache_dir
82
+ cache_dir: assignment.cache_dir,
83
+ launch_mode: resolved_launch_mode,
84
+ callback_pane: callback_pane
81
85
  )
82
86
  record_fork_pid_info(root_step, launch_result)
83
87
 
88
+ if callback_pane
89
+ puts "Fork subtree #{root_step.number} launched in callback mode." unless options[:quiet]
90
+ return
91
+ end
92
+
84
93
  refreshed = executor.status
85
94
  refreshed_state = refreshed[:state]
86
95
 
@@ -91,7 +100,7 @@ module Ace
91
100
  end
92
101
 
93
102
  unless refreshed_state.subtree_complete?(root_step.number)
94
- active = refreshed_state.in_progress_in_subtree(root_step.number).first || refreshed_state.current
103
+ active = refreshed_state.current_in_subtree(root_step.number) || refreshed_state.current
95
104
  active_msg = active ? " Current step: #{active.number} (#{active.name})." : ""
96
105
  last_msg = read_last_message(assignment.cache_dir, root_step.number)
97
106
  stall_reason = build_stall_reason(last_msg)
@@ -173,10 +182,10 @@ module Ace
173
182
  end
174
183
 
175
184
  # Fallback for legacy behavior when no root is explicitly scoped.
176
- raise Error, "No current step. Use --root <step-number> or --assignment <id>@<step-number>." unless current
185
+ raise Error, "No active step. Use --root <step-number> or --assignment <id>@<step-number>." unless current
177
186
 
178
187
  root = state.nearest_fork_ancestor(current.number)
179
- raise Error, "Current step is not in a forked subtree. Provide --root or --assignment <id>@<step-number>." unless root
188
+ raise Error, "Active step is not in a forked subtree. Provide --root or --assignment <id>@<step-number>." unless root
180
189
 
181
190
  root
182
191
  end
@@ -194,6 +203,39 @@ module Ace
194
203
  root_step.fork_provider || Ace::Assign.config.dig("execution", "provider") || Molecules::ForkSessionLauncher::DEFAULT_PROVIDER
195
204
  end
196
205
 
206
+ def ensure_not_same_scoped_refork!(assignment_id:, fork_root:)
207
+ return unless Molecules::ForkSessionLauncher.same_scoped_refork?(
208
+ assignment_id: assignment_id,
209
+ fork_root: fork_root
210
+ )
211
+
212
+ raise Error,
213
+ "Cannot fork-run subtree #{assignment_id}@#{fork_root}: already running inside that scoped subtree. Continue inline instead of calling fork-run again."
214
+ end
215
+
216
+ def resolved_launch_mode_for(root_step, cli_launch_mode)
217
+ explicit = cli_launch_mode&.to_s&.strip
218
+ return explicit unless explicit.nil? || explicit.empty?
219
+
220
+ root_step.fork_mode ||
221
+ Ace::Assign.config.dig("execution", "launch_mode") ||
222
+ Molecules::ForkSessionLauncher::DEFAULT_LAUNCH_MODE
223
+ end
224
+
225
+ def resolve_callback_pane(callback_enabled, launch_mode)
226
+ return nil unless callback_enabled
227
+
228
+ unless launch_mode == "tmux"
229
+ raise Error, "--callback requires tmux launch mode so the origin pane can be addressed."
230
+ end
231
+
232
+ pane = launcher.respond_to?(:callback_pane) ? launcher.callback_pane : nil
233
+ pane = pane.to_s.strip
234
+ raise Error, "--callback requires a resolvable origin tmux pane." if pane.empty?
235
+
236
+ pane
237
+ end
238
+
197
239
  def record_fork_pid_info(root_step, launch_result)
198
240
  pid_info = launch_result.is_a?(Hash) ? launch_result[:fork_pid_info] : nil
199
241
  return unless pid_info
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Assign
5
+ module CLI
6
+ module Commands
7
+ # Internal command used by tmux-backed fork panes to launch the provider session once.
8
+ class ForkSession < Ace::Support::Cli::Command
9
+ include Ace::Support::Cli::Base
10
+
11
+ desc "Run one provider-backed fork session for a subtree"
12
+
13
+ option :assignment, required: true, desc: "Target assignment ID"
14
+ option :root, required: true, desc: "Fork subtree root step number"
15
+ option :provider, desc: "LLM provider:model override (e.g., codex:gpt-5, claude:sonnet)"
16
+ option :cli_args, desc: "Extra CLI args for provider process"
17
+ option :timeout, type: :integer, desc: "Execution timeout in seconds"
18
+ option :cache_dir, desc: "Assignment cache directory override"
19
+ option :last_message_file, desc: "Explicit path for fork last-message capture"
20
+ option :session_meta_file, desc: "Explicit path for fork session metadata"
21
+
22
+ def initialize(launcher: nil)
23
+ super()
24
+ @launcher = launcher || Molecules::ForkSessionLauncher.new
25
+ end
26
+
27
+ def call(**options)
28
+ launcher.launch_provider_session(
29
+ assignment_id: options[:assignment],
30
+ fork_root: options[:root],
31
+ provider: options[:provider],
32
+ cli_args: options[:cli_args],
33
+ timeout: options[:timeout],
34
+ cache_dir: options[:cache_dir],
35
+ last_message_file: options[:last_message_file],
36
+ session_meta_file: options[:session_meta_file]
37
+ )
38
+
39
+ 0
40
+ rescue Error, Ace::LLM::Error => e
41
+ warn e.message
42
+ 1
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :launcher
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -93,7 +93,7 @@ module Ace
93
93
  # Header
94
94
  puts format(
95
95
  "%-#{COL_ID}s %-#{COL_NAME}s %-#{COL_STATUS}s %-#{COL_PROGRESS}s %-#{COL_STEP}s %s",
96
- "ID", "NAME", "STATUS", "PROGRESS", "CURRENT STEP", "UPDATED"
96
+ "ID", "NAME", "STATUS", "PROGRESS", "ACTIVE/NEXT", "UPDATED"
97
97
  )
98
98
  puts "-" * 95
99
99
 
@@ -104,7 +104,7 @@ module Ace
104
104
 
105
105
  name_display = truncate(info.name.to_s, COL_NAME - 1)
106
106
  state_display = STATE_LABELS[info.state] || info.state.to_s
107
- step_display = truncate(info.current_step, COL_STEP - 1)
107
+ step_display = truncate(info.step_focus, COL_STEP - 1)
108
108
  updated_display = format_relative_time(info.updated_at)
109
109
 
110
110
  puts format(
@@ -130,7 +130,8 @@ module Ace
130
130
  name: info.name,
131
131
  state: info.state.to_s,
132
132
  progress: info.progress,
133
- current_step: info.current_step,
133
+ active_steps: info.active_steps.map(&:name),
134
+ next_step: info.next_step&.name,
134
135
  updated_at: info.updated_at.iso8601,
135
136
  is_current: info.id == current_id
136
137
  }
@@ -19,7 +19,7 @@ module Ace
19
19
  def call(step_ref:, **options)
20
20
  target = resolve_assignment_target(options)
21
21
  executor = build_executor_for_target(target)
22
- result = executor.retry_step(step_ref)
22
+ result = executor.retry_step(step_ref, fork_root: target.scope)
23
23
 
24
24
  unless options[:quiet]
25
25
  retry_step = result[:retry]
@@ -29,9 +29,15 @@ module Ace
29
29
 
30
30
  started = result[:started]
31
31
  puts "Step #{started.number} (#{started.name}) started"
32
- puts
33
- puts "Instructions:"
34
- puts started.instructions
32
+ puts "Next: ace-assign step#{step_target_suffix(started.number, options[:assignment])}"
33
+ end
34
+
35
+ private
36
+
37
+ def step_target_suffix(step_number, assignment_target)
38
+ return " #{step_number}" if assignment_target.nil? || assignment_target.to_s.strip.empty?
39
+
40
+ %( #{step_number} --assignment "#{assignment_target}")
35
41
  end
36
42
  end
37
43
  end