ace-assign 0.53.4 → 0.55.2

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 +25 -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 +31 -17
  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.2
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-08-12 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-cli
@@ -29,56 +29,56 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '0.29'
32
+ version: '0.31'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0.29'
39
+ version: '0.31'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: ace-support-config
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0.9'
46
+ version: '0.17'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '0.9'
53
+ version: '0.17'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: ace-support-nav
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '0.25'
60
+ version: '0.28'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '0.25'
67
+ version: '0.28'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: ace-b36ts
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '0.13'
74
+ version: '0.14'
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '0.13'
81
+ version: '0.14'
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: ace-support-markdown
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -99,42 +99,56 @@ dependencies:
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: '0.34'
102
+ version: '0.38'
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '0.34'
109
+ version: '0.38'
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: ace-task
112
112
  requirement: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: '0.31'
116
+ version: '0.37'
117
117
  type: :runtime
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: '0.31'
123
+ version: '0.37'
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
127
141
  requirements:
128
142
  - - "~>"
129
143
  - !ruby/object:Gem::Version
130
- version: '0.13'
144
+ version: '0.14'
131
145
  type: :development
132
146
  prerelease: false
133
147
  version_requirements: !ruby/object:Gem::Requirement
134
148
  requirements:
135
149
  - - "~>"
136
150
  - !ruby/object:Gem::Version
137
- version: '0.13'
151
+ version: '0.14'
138
152
  - !ruby/object:Gem::Dependency
139
153
  name: minitest
140
154
  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