ace-assign 0.53.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/steps/split-subtree-root.step.yml +4 -2
- data/.ace-defaults/assign/config.yml +1 -0
- data/.ace-defaults/assign/presets/work-on-task.yml +3 -0
- data/CHANGELOG.md +15 -0
- data/docs/demo/fork-provider.cast +814 -937
- data/docs/demo/fork-provider.gif +0 -0
- data/docs/demo/fork-provider.recording.json +15 -17
- data/docs/demo/fork-provider.tape.yml +16 -4
- data/docs/usage.md +30 -7
- data/handbook/guides/fork-context.g.md +29 -5
- data/handbook/skills/as-assign-drive/SKILL.md +2 -2
- data/handbook/workflow-instructions/assign/drive.wf.md +109 -36
- data/handbook/workflow-instructions/assign/prepare.wf.md +5 -0
- data/lib/ace/assign/atoms/preset_expander.rb +4 -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 +49 -18
- 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 +26 -5
- data/lib/ace/assign/cli/commands/fork_run.rb +56 -17
- 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/status.rb +60 -34
- data/lib/ace/assign/cli/commands/step.rb +4 -4
- data/lib/ace/assign/cli.rb +1 -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 +13 -3
- data/lib/ace/assign/molecules/fork_session_launcher.rb +76 -60
- 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 +132 -82
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +1 -0
- metadata +17 -3
- data/lib/ace/assign/molecules/tmux_fork_runner.rb +0 -191
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-assign
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.55.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-26 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|
|
@@ -121,6 +121,20 @@ dependencies:
|
|
|
121
121
|
- - "~>"
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
123
|
version: '0.31'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: ace-tmux
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0.17'
|
|
131
|
+
type: :runtime
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0.17'
|
|
124
138
|
- !ruby/object:Gem::Dependency
|
|
125
139
|
name: ace-support-test-helpers
|
|
126
140
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -295,7 +309,7 @@ files:
|
|
|
295
309
|
- lib/ace/assign/molecules/skill_assign_source_resolver.rb
|
|
296
310
|
- lib/ace/assign/molecules/step_renumberer.rb
|
|
297
311
|
- lib/ace/assign/molecules/step_writer.rb
|
|
298
|
-
- lib/ace/assign/molecules/
|
|
312
|
+
- lib/ace/assign/molecules/tmux_control_surface_runner.rb
|
|
299
313
|
- lib/ace/assign/organisms/assignment_executor.rb
|
|
300
314
|
- lib/ace/assign/organisms/task_assignment_creator.rb
|
|
301
315
|
- lib/ace/assign/version.rb
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "open3"
|
|
5
|
-
require "shellwords"
|
|
6
|
-
require "yaml"
|
|
7
|
-
|
|
8
|
-
module Ace
|
|
9
|
-
module Assign
|
|
10
|
-
module Molecules
|
|
11
|
-
# Minimal tmux integration for forked subtree execution.
|
|
12
|
-
class TmuxForkRunner
|
|
13
|
-
DEFAULT_POLL_INTERVAL = 0.2
|
|
14
|
-
|
|
15
|
-
def initialize(tmux_binary: "tmux")
|
|
16
|
-
@tmux_binary = tmux_binary
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def tmux_context?
|
|
20
|
-
!current_session.to_s.strip.empty?
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def current_session
|
|
24
|
-
explicit = ENV["ACE_TMUX_SESSION"].to_s.strip
|
|
25
|
-
return explicit unless explicit.empty?
|
|
26
|
-
return nil if ENV["TMUX"].to_s.strip.empty?
|
|
27
|
-
|
|
28
|
-
capture([tmux_binary, "display-message", "-p", "#S"]).stdout
|
|
29
|
-
rescue
|
|
30
|
-
nil
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def current_window
|
|
34
|
-
explicit = ENV["ACE_ASSIGN_FORK_WINDOW"].to_s.strip
|
|
35
|
-
return explicit unless explicit.empty?
|
|
36
|
-
session = ENV["ACE_TMUX_SESSION"].to_s.strip
|
|
37
|
-
if !session.empty?
|
|
38
|
-
window = capture([tmux_binary, "display-message", "-t", "#{session}:", "-p", "#W"]).stdout
|
|
39
|
-
return window unless window.empty?
|
|
40
|
-
|
|
41
|
-
return active_window_name(session)
|
|
42
|
-
end
|
|
43
|
-
return nil if ENV["TMUX"].to_s.strip.empty?
|
|
44
|
-
|
|
45
|
-
capture([tmux_binary, "display-message", "-p", "#W"]).stdout
|
|
46
|
-
rescue
|
|
47
|
-
nil
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def fork_window_name(base_window)
|
|
51
|
-
base = base_window.to_s.strip
|
|
52
|
-
return base if base.end_with?("-fs")
|
|
53
|
-
|
|
54
|
-
"#{base}-fs"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def ensure_window(session:, name:, root:)
|
|
58
|
-
return {created: false, target: "#{session}:#{name}"} if window_exists?(session: session, name: name)
|
|
59
|
-
|
|
60
|
-
result = capture([tmux_binary, "new-window", "-t", "#{session}:", "-n", name, "-c", File.expand_path(root)])
|
|
61
|
-
raise Error, "Failed to create tmux fork window #{name}: #{result.stderr}" unless result.success?
|
|
62
|
-
|
|
63
|
-
{created: true, target: "#{session}:#{name}"}
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def prepare_pane(session:, window:, root:, keep_existing:)
|
|
67
|
-
target = "#{session}:#{window}"
|
|
68
|
-
if keep_existing
|
|
69
|
-
pane = first_pane(target)
|
|
70
|
-
set_pane_remain_on_exit(pane)
|
|
71
|
-
select_layout(target)
|
|
72
|
-
return pane
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
result = capture([tmux_binary, "split-window", "-t", target, "-c", File.expand_path(root), "-P", "-F", '#{pane_id}'])
|
|
76
|
-
raise Error, "Failed to create tmux fork pane in #{window}: #{result.stderr}" unless result.success?
|
|
77
|
-
|
|
78
|
-
pane = result.stdout
|
|
79
|
-
set_pane_remain_on_exit(pane)
|
|
80
|
-
select_layout(target)
|
|
81
|
-
pane
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def select_window(session:, window:)
|
|
85
|
-
run!([tmux_binary, "select-window", "-t", "#{session}:#{window}"], "select tmux fork window #{window}")
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def run_script_in_pane(pane_target:, script_path:)
|
|
89
|
-
command = "bash #{Shellwords.escape(File.expand_path(script_path))}"
|
|
90
|
-
run!([tmux_binary, "send-keys", "-t", pane_target, command, "Enter"], "send tmux fork command")
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def wait_for_completion(status_file:, timeout:)
|
|
94
|
-
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout.to_i
|
|
95
|
-
until File.exist?(status_file)
|
|
96
|
-
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
97
|
-
raise Error, "Timed out waiting for tmux fork pane to finish (#{File.basename(status_file)})"
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
sleep(DEFAULT_POLL_INTERVAL)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
status = File.read(status_file).strip
|
|
104
|
-
Integer(status)
|
|
105
|
-
rescue ArgumentError
|
|
106
|
-
raise Error, "Invalid tmux fork status file: #{status_file}"
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def merge_tmux_metadata(session_meta_file:, session:, window:, pane:)
|
|
110
|
-
data = if File.exist?(session_meta_file)
|
|
111
|
-
YAML.safe_load_file(session_meta_file) || {}
|
|
112
|
-
else
|
|
113
|
-
{}
|
|
114
|
-
end
|
|
115
|
-
data["launch_mode"] = "tmux"
|
|
116
|
-
data["tmux_session"] = session
|
|
117
|
-
data["tmux_window"] = window
|
|
118
|
-
data["tmux_pane_id"] = pane
|
|
119
|
-
File.write(session_meta_file, data.to_yaml)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
private
|
|
123
|
-
|
|
124
|
-
attr_reader :tmux_binary
|
|
125
|
-
|
|
126
|
-
def window_exists?(session:, name:)
|
|
127
|
-
result = capture([tmux_binary, "list-windows", "-t", session, "-F", '#{window_name}'])
|
|
128
|
-
return false unless result.success?
|
|
129
|
-
|
|
130
|
-
result.stdout_lines.any? { |value| value == name }
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def active_window_name(session)
|
|
134
|
-
result = capture([tmux_binary, "list-windows", "-t", session, "-F", '#{window_active} #{window_name}'])
|
|
135
|
-
return nil unless result.success?
|
|
136
|
-
|
|
137
|
-
active = result.stdout_lines.find { |line| line.start_with?("1 ") }
|
|
138
|
-
active&.sub(/\A1\s+/, "") || result.stdout_lines.first&.sub(/\A[01]\s+/, "")
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def first_pane(target)
|
|
142
|
-
result = capture([tmux_binary, "list-panes", "-t", target, "-F", '#{pane_id}'])
|
|
143
|
-
raise Error, "Failed to inspect panes for #{target}: #{result.stderr}" unless result.success?
|
|
144
|
-
|
|
145
|
-
pane = result.stdout_lines.first
|
|
146
|
-
raise Error, "No panes found for #{target}" if pane.to_s.empty?
|
|
147
|
-
|
|
148
|
-
pane
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def set_pane_remain_on_exit(pane_target)
|
|
152
|
-
run!([tmux_binary, "set-option", "-p", "-t", pane_target, "remain-on-exit", "on"], "enable remain-on-exit")
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def select_layout(target)
|
|
156
|
-
run!([tmux_binary, "select-layout", "-t", target, "tiled"], "apply tiled layout")
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def run!(cmd, action)
|
|
160
|
-
result = capture(cmd)
|
|
161
|
-
return if result.success?
|
|
162
|
-
|
|
163
|
-
raise Error, "Failed to #{action}: #{result.stderr}"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def capture(cmd)
|
|
167
|
-
stdout, stderr, status = Open3.capture3(*cmd)
|
|
168
|
-
Result.new(stdout: stdout, stderr: stderr, status: status)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
Result = Struct.new(:stdout, :stderr, :status, keyword_init: true) do
|
|
172
|
-
def success?
|
|
173
|
-
status.success?
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def stdout
|
|
177
|
-
self[:stdout].to_s.strip
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def stderr
|
|
181
|
-
self[:stderr].to_s.strip
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def stdout_lines
|
|
185
|
-
stdout.split("\n").map(&:strip).reject(&:empty?)
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|