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
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
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
|
|
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, "
|
|
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", "
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|