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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/split-subtree-root.step.yml +4 -2
  3. data/.ace-defaults/assign/config.yml +1 -0
  4. data/.ace-defaults/assign/presets/work-on-task.yml +3 -0
  5. data/CHANGELOG.md +15 -0
  6. data/docs/demo/fork-provider.cast +814 -937
  7. data/docs/demo/fork-provider.gif +0 -0
  8. data/docs/demo/fork-provider.recording.json +15 -17
  9. data/docs/demo/fork-provider.tape.yml +16 -4
  10. data/docs/usage.md +30 -7
  11. data/handbook/guides/fork-context.g.md +29 -5
  12. data/handbook/skills/as-assign-drive/SKILL.md +2 -2
  13. data/handbook/workflow-instructions/assign/drive.wf.md +109 -36
  14. data/handbook/workflow-instructions/assign/prepare.wf.md +5 -0
  15. data/lib/ace/assign/atoms/preset_expander.rb +4 -0
  16. data/lib/ace/assign/atoms/tree_formatter.rb +2 -2
  17. data/lib/ace/assign/cli/commands/add.rb +20 -11
  18. data/lib/ace/assign/cli/commands/assignment_target.rb +49 -18
  19. data/lib/ace/assign/cli/commands/create.rb +1 -1
  20. data/lib/ace/assign/cli/commands/fail.rb +1 -1
  21. data/lib/ace/assign/cli/commands/finish.rb +26 -5
  22. data/lib/ace/assign/cli/commands/fork_run.rb +56 -17
  23. data/lib/ace/assign/cli/commands/list.rb +4 -3
  24. data/lib/ace/assign/cli/commands/retry_cmd.rb +1 -1
  25. data/lib/ace/assign/cli/commands/status.rb +60 -34
  26. data/lib/ace/assign/cli/commands/step.rb +4 -4
  27. data/lib/ace/assign/cli.rb +1 -1
  28. data/lib/ace/assign/models/assignment_info.rb +33 -4
  29. data/lib/ace/assign/models/queue_state.rb +101 -39
  30. data/lib/ace/assign/models/step.rb +13 -3
  31. data/lib/ace/assign/molecules/fork_session_launcher.rb +76 -60
  32. data/lib/ace/assign/molecules/step_writer.rb +3 -3
  33. data/lib/ace/assign/molecules/tmux_control_surface_runner.rb +249 -0
  34. data/lib/ace/assign/organisms/assignment_executor.rb +132 -82
  35. data/lib/ace/assign/version.rb +1 -1
  36. data/lib/ace/assign.rb +1 -0
  37. metadata +17 -3
  38. 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.53.4
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-20 00:00:00.000000000 Z
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/tmux_fork_runner.rb
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