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
data/lib/ace/assign/cli.rb
CHANGED
|
@@ -31,6 +31,7 @@ require_relative "molecules/step_writer"
|
|
|
31
31
|
require_relative "molecules/step_renumberer"
|
|
32
32
|
require_relative "molecules/skill_assign_source_resolver"
|
|
33
33
|
require_relative "molecules/fork_session_launcher"
|
|
34
|
+
require_relative "molecules/tmux_fork_runner"
|
|
34
35
|
require_relative "molecules/preset_inferrer"
|
|
35
36
|
|
|
36
37
|
# Organisms
|
|
@@ -40,6 +41,7 @@ require_relative "organisms/assignment_executor"
|
|
|
40
41
|
require_relative "cli/commands/create"
|
|
41
42
|
require_relative "cli/commands/assignment_target"
|
|
42
43
|
require_relative "cli/commands/status"
|
|
44
|
+
require_relative "cli/commands/step"
|
|
43
45
|
require_relative "cli/commands/start"
|
|
44
46
|
require_relative "cli/commands/finish"
|
|
45
47
|
require_relative "cli/commands/fail"
|
|
@@ -48,6 +50,7 @@ require_relative "cli/commands/retry_cmd"
|
|
|
48
50
|
require_relative "cli/commands/list"
|
|
49
51
|
require_relative "cli/commands/select"
|
|
50
52
|
require_relative "cli/commands/fork_run"
|
|
53
|
+
require_relative "cli/commands/fork_session"
|
|
51
54
|
|
|
52
55
|
module Ace
|
|
53
56
|
module Assign
|
|
@@ -61,6 +64,7 @@ module Ace
|
|
|
61
64
|
REGISTERED_COMMANDS = [
|
|
62
65
|
["create", "Create assignment from preset or YAML"],
|
|
63
66
|
["status", "Show assignment status"],
|
|
67
|
+
["step", "Show step instructions"],
|
|
64
68
|
["start", "Start next workable step"],
|
|
65
69
|
["finish", "Complete current step with report"],
|
|
66
70
|
["fail", "Mark step as failed"],
|
|
@@ -73,7 +77,8 @@ module Ace
|
|
|
73
77
|
|
|
74
78
|
HELP_EXAMPLES = [
|
|
75
79
|
"ace-assign create --preset review # Start review assignment",
|
|
76
|
-
"ace-assign status #
|
|
80
|
+
"ace-assign status # Compact queue progress",
|
|
81
|
+
"ace-assign step # Current or next step instructions",
|
|
77
82
|
"ace-assign start # Start next workable step",
|
|
78
83
|
"ace-assign finish --message done.md # Complete active step",
|
|
79
84
|
"cat report.md | ace-assign finish # Complete step via stdin",
|
|
@@ -115,6 +120,7 @@ module Ace
|
|
|
115
120
|
# Register commands (wrapped to capture exit codes)
|
|
116
121
|
register "create", wrap_command(Commands::Create)
|
|
117
122
|
register "status", wrap_command(Commands::Status)
|
|
123
|
+
register "step", wrap_command(Commands::Step)
|
|
118
124
|
register "start", wrap_command(Commands::Start)
|
|
119
125
|
register "finish", wrap_command(Commands::Finish)
|
|
120
126
|
register "fail", wrap_command(Commands::Fail)
|
|
@@ -123,6 +129,7 @@ module Ace
|
|
|
123
129
|
register "list", wrap_command(Commands::List)
|
|
124
130
|
register "select", wrap_command(Commands::Select)
|
|
125
131
|
register "fork-run", wrap_command(Commands::ForkRun)
|
|
132
|
+
register "fork-session", wrap_command(Commands::ForkSession)
|
|
126
133
|
|
|
127
134
|
# Register version command
|
|
128
135
|
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
@@ -23,7 +23,7 @@ module Ace
|
|
|
23
23
|
VALID_CONTEXTS = %w[fork].freeze
|
|
24
24
|
|
|
25
25
|
attr_reader :number, :name, :status, :instructions, :report, :error,
|
|
26
|
-
:started_at, :completed_at, :added_by, :parent, :file_path, :skill, :context,
|
|
26
|
+
:started_at, :completed_at, :added_by, :parent, :file_path, :source, :skill, :context,
|
|
27
27
|
:workflow,
|
|
28
28
|
:batch_parent, :parallel, :max_parallel, :fork_retry_limit, :fork_options,
|
|
29
29
|
:fork_launch_pid, :fork_tracked_pids, :fork_pid_updated_at, :fork_pid_file,
|
|
@@ -54,7 +54,7 @@ module Ace
|
|
|
54
54
|
# @param stall_reason [String, nil] Last agent message captured when fork stalled
|
|
55
55
|
def initialize(number:, name:, status:, instructions:, report: nil, error: nil,
|
|
56
56
|
started_at: nil, completed_at: nil, added_by: nil, parent: nil,
|
|
57
|
-
file_path: nil, skill: nil, workflow: nil, context: nil,
|
|
57
|
+
file_path: nil, source: nil, skill: nil, workflow: nil, context: nil,
|
|
58
58
|
batch_parent: nil, parallel: nil, max_parallel: nil, fork_retry_limit: nil,
|
|
59
59
|
fork_options: nil,
|
|
60
60
|
fork_launch_pid: nil, fork_tracked_pids: nil, fork_pid_updated_at: nil,
|
|
@@ -78,6 +78,7 @@ module Ace
|
|
|
78
78
|
@added_by = added_by&.freeze
|
|
79
79
|
@parent = parent&.freeze
|
|
80
80
|
@file_path = file_path&.freeze
|
|
81
|
+
@source = source&.freeze
|
|
81
82
|
@skill = skill&.freeze
|
|
82
83
|
@workflow = workflow&.freeze
|
|
83
84
|
@context = context&.freeze
|
|
@@ -141,6 +142,7 @@ module Ace
|
|
|
141
142
|
{
|
|
142
143
|
"name" => name,
|
|
143
144
|
"status" => status.to_s,
|
|
145
|
+
"source" => source,
|
|
144
146
|
"skill" => skill,
|
|
145
147
|
"workflow" => workflow,
|
|
146
148
|
"context" => context,
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "ace/llm"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require "rbconfig"
|
|
6
|
+
require "shellwords"
|
|
5
7
|
|
|
6
8
|
module Ace
|
|
7
9
|
module Assign
|
|
@@ -10,10 +12,15 @@ module Ace
|
|
|
10
12
|
class ForkSessionLauncher
|
|
11
13
|
DEFAULT_PROVIDER = "claude:sonnet"
|
|
12
14
|
DEFAULT_TIMEOUT = 1800
|
|
15
|
+
DEFAULT_LAUNCH_MODE = "auto"
|
|
16
|
+
VALID_LAUNCH_MODES = %w[auto headless tmux].freeze
|
|
17
|
+
TMUX_POLL_INTERVAL = 0.5
|
|
13
18
|
|
|
14
|
-
def initialize(config: nil, query_interface: Ace::LLM::QueryInterface)
|
|
19
|
+
def initialize(config: nil, query_interface: Ace::LLM::QueryInterface, tmux_runner: nil, interactive_builder: nil)
|
|
15
20
|
@config = config || Ace::Assign.config
|
|
16
21
|
@query_interface = query_interface
|
|
22
|
+
@tmux_runner = tmux_runner || TmuxForkRunner.new
|
|
23
|
+
@interactive_builder = interactive_builder || Ace::LLM::Molecules::InteractiveCommandBuilder.new
|
|
17
24
|
end
|
|
18
25
|
|
|
19
26
|
# Launch forked subtree execution synchronously.
|
|
@@ -24,16 +31,45 @@ module Ace
|
|
|
24
31
|
# @param cli_args [String, nil] Optional provider CLI args
|
|
25
32
|
# @param timeout [Integer, nil] Optional timeout override (seconds)
|
|
26
33
|
# @param cache_dir [String, nil] Assignment cache directory for last-message capture
|
|
34
|
+
# @param launch_mode [String, nil] Launch mode override (auto|headless|tmux)
|
|
27
35
|
# @return [Hash] QueryInterface response
|
|
28
|
-
def launch(assignment_id:, fork_root:, provider: nil, cli_args: nil, timeout: nil, cache_dir: nil)
|
|
36
|
+
def launch(assignment_id:, fork_root:, provider: nil, cli_args: nil, timeout: nil, cache_dir: nil, launch_mode: nil)
|
|
37
|
+
resolved_provider = provider || config.dig("execution", "provider") || DEFAULT_PROVIDER
|
|
38
|
+
resolved_timeout = timeout || config.dig("execution", "timeout") || DEFAULT_TIMEOUT
|
|
39
|
+
resolved_mode = resolve_launch_mode(launch_mode)
|
|
40
|
+
|
|
41
|
+
if resolved_mode == "tmux"
|
|
42
|
+
launch_tmux(
|
|
43
|
+
assignment_id: assignment_id,
|
|
44
|
+
fork_root: fork_root,
|
|
45
|
+
provider: resolved_provider,
|
|
46
|
+
cli_args: cli_args,
|
|
47
|
+
timeout: resolved_timeout,
|
|
48
|
+
cache_dir: cache_dir
|
|
49
|
+
)
|
|
50
|
+
else
|
|
51
|
+
launch_provider_session(
|
|
52
|
+
assignment_id: assignment_id,
|
|
53
|
+
fork_root: fork_root,
|
|
54
|
+
provider: resolved_provider,
|
|
55
|
+
cli_args: cli_args,
|
|
56
|
+
timeout: resolved_timeout,
|
|
57
|
+
cache_dir: cache_dir
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def launch_provider_session(assignment_id:, fork_root:, provider:, cli_args: nil, timeout: nil, cache_dir: nil,
|
|
63
|
+
last_message_file: nil, session_meta_file: nil)
|
|
29
64
|
resolved_provider = provider || config.dig("execution", "provider") || DEFAULT_PROVIDER
|
|
30
65
|
resolved_timeout = timeout || config.dig("execution", "timeout") || DEFAULT_TIMEOUT
|
|
31
66
|
scoped_assignment = "#{assignment_id}@#{fork_root}"
|
|
32
|
-
|
|
67
|
+
prompt = "/as-assign-drive #{scoped_assignment}"
|
|
68
|
+
last_msg_file = last_message_file || build_last_message_file(cache_dir, fork_root)
|
|
33
69
|
|
|
34
70
|
result = query_interface.query(
|
|
35
71
|
resolved_provider,
|
|
36
|
-
|
|
72
|
+
prompt,
|
|
37
73
|
system: nil,
|
|
38
74
|
cli_args: cli_args,
|
|
39
75
|
timeout: resolved_timeout,
|
|
@@ -49,7 +85,7 @@ module Ace
|
|
|
49
85
|
File.write(last_msg_file, result[:text]) if existing.empty?
|
|
50
86
|
end
|
|
51
87
|
|
|
52
|
-
write_session_metadata(last_msg_file, result, prompt:
|
|
88
|
+
write_session_metadata(last_msg_file, result, prompt: prompt, session_meta_file: session_meta_file)
|
|
53
89
|
|
|
54
90
|
result
|
|
55
91
|
rescue Ace::LLM::Error => e
|
|
@@ -58,9 +94,94 @@ module Ace
|
|
|
58
94
|
|
|
59
95
|
private
|
|
60
96
|
|
|
61
|
-
attr_reader :config, :query_interface
|
|
97
|
+
attr_reader :config, :query_interface, :tmux_runner, :interactive_builder
|
|
98
|
+
|
|
99
|
+
def resolve_launch_mode(explicit_mode)
|
|
100
|
+
mode = explicit_mode.to_s.strip
|
|
101
|
+
mode = DEFAULT_LAUNCH_MODE if mode.empty?
|
|
102
|
+
unless VALID_LAUNCH_MODES.include?(mode)
|
|
103
|
+
raise Error, "Invalid launch mode '#{mode}'. Expected one of: #{VALID_LAUNCH_MODES.join(', ')}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
return mode unless mode == "auto"
|
|
107
|
+
|
|
108
|
+
tmux_runner.tmux_context? ? "tmux" : "headless"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def launch_tmux(assignment_id:, fork_root:, provider:, cli_args:, timeout:, cache_dir:)
|
|
112
|
+
session = tmux_runner.current_session
|
|
113
|
+
raise Error, "Launch mode tmux requires an active tmux session (TMUX or ACE_TMUX_SESSION)." unless session
|
|
114
|
+
raise Error, "Tmux launch requires assignment cache_dir for subtree polling." if cache_dir.to_s.strip.empty?
|
|
115
|
+
|
|
116
|
+
current_window = tmux_runner.current_window
|
|
117
|
+
raise Error, "Could not resolve current tmux window for fork launch." if current_window.to_s.strip.empty?
|
|
118
|
+
|
|
119
|
+
fork_window = ENV["ACE_ASSIGN_FORK_WINDOW"].to_s.strip
|
|
120
|
+
fork_window = tmux_runner.fork_window_name(current_window) if fork_window.empty?
|
|
121
|
+
|
|
122
|
+
window_info = tmux_runner.ensure_window(session: session, name: fork_window, root: Dir.pwd)
|
|
123
|
+
pane_target = tmux_runner.prepare_pane(
|
|
124
|
+
session: session,
|
|
125
|
+
window: fork_window,
|
|
126
|
+
root: Dir.pwd,
|
|
127
|
+
keep_existing: window_info[:created]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
session_meta_file = build_session_meta_file(cache_dir, fork_root)
|
|
131
|
+
prompt = "/as-assign-drive #{assignment_id}@#{fork_root}"
|
|
132
|
+
invocation = interactive_builder.build(
|
|
133
|
+
provider_model: provider,
|
|
134
|
+
prompt: prompt,
|
|
135
|
+
cli_args: cli_args
|
|
136
|
+
)
|
|
137
|
+
script_path = build_tmux_wrapper(
|
|
138
|
+
assignment_id: assignment_id,
|
|
139
|
+
fork_root: fork_root,
|
|
140
|
+
provider: provider,
|
|
141
|
+
cli_args: cli_args,
|
|
142
|
+
timeout: timeout,
|
|
143
|
+
session_meta_file: session_meta_file,
|
|
144
|
+
session: session,
|
|
145
|
+
fork_window: fork_window,
|
|
146
|
+
visible_handoff: invocation[:prompt]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
tmux_runner.run_script_in_pane(pane_target: pane_target, script_path: script_path)
|
|
150
|
+
tmux_runner.select_window(session: session, window: fork_window) if window_info[:created] && current_window != fork_window
|
|
151
|
+
write_tmux_launch_metadata(
|
|
152
|
+
session_meta_file: session_meta_file,
|
|
153
|
+
provider: invocation[:provider],
|
|
154
|
+
model: invocation[:model],
|
|
155
|
+
prompt: invocation[:prompt]
|
|
156
|
+
)
|
|
157
|
+
tmux_runner.merge_tmux_metadata(
|
|
158
|
+
session_meta_file: session_meta_file,
|
|
159
|
+
session: session,
|
|
160
|
+
window: fork_window,
|
|
161
|
+
pane: pane_target
|
|
162
|
+
)
|
|
163
|
+
wait_for_subtree_terminal(
|
|
164
|
+
assignment_id: assignment_id,
|
|
165
|
+
fork_root: fork_root,
|
|
166
|
+
cache_dir: cache_dir,
|
|
167
|
+
timeout: timeout
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
{tmux: true, pane_target: pane_target}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def write_tmux_launch_metadata(session_meta_file:, provider:, model:, prompt:)
|
|
174
|
+
detected = detect_provider_session(provider, prompt)
|
|
175
|
+
meta = {}
|
|
176
|
+
meta = YAML.safe_load_file(session_meta_file) || {} if File.exist?(session_meta_file)
|
|
177
|
+
meta["provider"] ||= provider
|
|
178
|
+
meta["model"] ||= model
|
|
179
|
+
meta["session_id"] ||= detected&.dig(:session_id)
|
|
180
|
+
meta["launched_at"] ||= Time.now.utc.iso8601
|
|
181
|
+
File.write(session_meta_file, meta.to_yaml) unless meta.empty?
|
|
182
|
+
end
|
|
62
183
|
|
|
63
|
-
def write_session_metadata(last_msg_file, result, prompt:)
|
|
184
|
+
def write_session_metadata(last_msg_file, result, prompt:, session_meta_file: nil)
|
|
64
185
|
return unless last_msg_file
|
|
65
186
|
|
|
66
187
|
session_id = result.dig(:metadata, :session_id)
|
|
@@ -70,7 +191,7 @@ module Ace
|
|
|
70
191
|
session_id = detected&.dig(:session_id)
|
|
71
192
|
end
|
|
72
193
|
|
|
73
|
-
session_meta_file
|
|
194
|
+
session_meta_file ||= last_msg_file.sub(/-last-message\.md$/, "-session.yml")
|
|
74
195
|
meta = {
|
|
75
196
|
"session_id" => session_id,
|
|
76
197
|
"provider" => result[:provider],
|
|
@@ -96,6 +217,66 @@ module Ace
|
|
|
96
217
|
FileUtils.mkdir_p(sessions_dir)
|
|
97
218
|
File.join(sessions_dir, "#{fork_root}-last-message.md")
|
|
98
219
|
end
|
|
220
|
+
|
|
221
|
+
def build_session_meta_file(cache_dir, fork_root)
|
|
222
|
+
return nil unless cache_dir
|
|
223
|
+
|
|
224
|
+
sessions_dir = File.join(cache_dir, "sessions")
|
|
225
|
+
FileUtils.mkdir_p(sessions_dir)
|
|
226
|
+
File.join(sessions_dir, "#{fork_root}-session.yml")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def build_tmux_wrapper(assignment_id:, fork_root:, provider:, cli_args:, timeout:, session_meta_file:, session:, fork_window:, visible_handoff:)
|
|
230
|
+
sessions_dir = File.dirname(session_meta_file)
|
|
231
|
+
FileUtils.mkdir_p(sessions_dir)
|
|
232
|
+
script_path = File.join(sessions_dir, "#{fork_root}-tmux-launch.sh")
|
|
233
|
+
|
|
234
|
+
command = [
|
|
235
|
+
"ace-llm", provider, "/as-assign-drive #{assignment_id}@#{fork_root}",
|
|
236
|
+
"--interactive"
|
|
237
|
+
]
|
|
238
|
+
command.concat(["--cli-args", cli_args]) if cli_args && !cli_args.strip.empty?
|
|
239
|
+
|
|
240
|
+
script = <<~BASH
|
|
241
|
+
#!/usr/bin/env bash
|
|
242
|
+
set -uo pipefail
|
|
243
|
+
cd #{Shellwords.escape(Dir.pwd)}
|
|
244
|
+
export PROJECT_ROOT_PATH=#{Shellwords.escape(Dir.pwd)}
|
|
245
|
+
export ACE_TMUX_SESSION=#{Shellwords.escape(session)}
|
|
246
|
+
export ACE_ASSIGN_LAUNCH_MODE=tmux
|
|
247
|
+
export ACE_ASSIGN_FORK_WINDOW=#{Shellwords.escape(fork_window)}
|
|
248
|
+
printf '%s\n' #{Shellwords.escape(visible_handoff.to_s)}
|
|
249
|
+
exec #{command.map { |part| Shellwords.escape(part) }.join(" ")}
|
|
250
|
+
BASH
|
|
251
|
+
|
|
252
|
+
File.write(script_path, script)
|
|
253
|
+
FileUtils.chmod(0o755, script_path)
|
|
254
|
+
script_path
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def wait_for_subtree_terminal(assignment_id:, fork_root:, cache_dir:, timeout:)
|
|
258
|
+
assignment = Models::Assignment.new(
|
|
259
|
+
id: assignment_id,
|
|
260
|
+
name: "fork",
|
|
261
|
+
created_at: Time.now.utc,
|
|
262
|
+
source_config: "fork-run",
|
|
263
|
+
cache_dir: cache_dir
|
|
264
|
+
)
|
|
265
|
+
scanner = QueueScanner.new
|
|
266
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout.to_i
|
|
267
|
+
|
|
268
|
+
loop do
|
|
269
|
+
state = scanner.scan(assignment.steps_dir, assignment: assignment)
|
|
270
|
+
return state if state.subtree_complete?(fork_root)
|
|
271
|
+
raise Error, "Fork session execution failed in tmux subtree #{fork_root}." if state.subtree_failed?(fork_root)
|
|
272
|
+
|
|
273
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
274
|
+
raise Error, "Timed out waiting for tmux fork subtree #{fork_root} to reach a terminal state."
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
sleep(TMUX_POLL_INTERVAL)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
99
280
|
end
|
|
100
281
|
end
|
|
101
282
|
end
|