ace-assign 0.62.0 → 0.63.1
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/config.yml +1 -1
- data/CHANGELOG.md +18 -1
- data/README.md +1 -1
- data/docs/usage.md +10 -11
- data/handbook/workflow-instructions/assign/drive.wf.md +4 -4
- data/lib/ace/assign/cli/commands/fork_run.rb +6 -6
- data/lib/ace/assign/cli.rb +1 -1
- data/lib/ace/assign/molecules/fork_session_launcher.rb +55 -53
- data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +158 -0
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +1 -1
- metadata +30 -2
- data/lib/ace/assign/molecules/tmux_control_surface_runner.rb +0 -249
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d743c208ce34b070929c7c5cc2acdd200289e7a82e3e2acd62129ec1d22daa5
|
|
4
|
+
data.tar.gz: 3b4e13b527152e703f7a489f0c7b123b0676c682f6eccb3bd6f30680cf96ace2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eeba37f93e80112453a79322c0496b6dc5753f5da072556575836a142827c2a3724848978eec0025598babbe1077def70f77fee49df79dd5ae6f54ea668fdd2d
|
|
7
|
+
data.tar.gz: f78cf53f13cbb76fb67d7a72756189063fa39d0dddb8eb3b55c28e7acd16377e0fc0a486b126fafe926791905871610800f719d166a55a1e2e65431fe62a3724
|
data/CHANGELOG.md
CHANGED
|
@@ -7,12 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.63.1] - 2026-10-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Install both Herdr and tmux runtime adapters for standalone runtime switching, through an acyclic dependency graph.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Run terminal fork invocations as children of the prepared shell, retaining the same writable pane after a command exits.
|
|
19
|
+
|
|
20
|
+
- Managed assignment status JSON tests run against test-owned repositories and evidence journals instead of the invoking checkout's journal, evidence ref, or worktree registrations (task 8ws.t.ibk). A sentinel-based invariance test asserts the ambient repository's refs, registrations, and files stay untouched, and fixture journals cover absent, seed-only, and stale-checkout states.
|
|
21
|
+
|
|
22
|
+
## [0.63.0] - 2026-10-04
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- Runtime-neutral fork launches: `launch_mode` gains `herdr` and `auto` detects the live terminal runtime (tmux first, then herdr; headless only outside any runtime). `TmuxControlSurfaceRunner` is replaced by the contract-backed `RuntimeControlSurfaceRunner`; fork windows/panes, sends, captures, and session metadata are runtime-neutral (`runtime`/`session`/`window`/`window_id`/`pane` replace the `tmux_*` metadata keys), and `--callback` works under herdr. Fork children now receive `ACE_RUNTIME` instead of `ACE_TMUX_SESSION` propagation, and the drive workflow callback rule prescribes `ace-runtime send`.
|
|
26
|
+
- New dependency: `ace-runtime` (~> 0.1); `ace-tmux` is kept to install the tmux adapter.
|
|
27
|
+
|
|
10
28
|
## [0.62.0] - 2026-10-04
|
|
11
29
|
|
|
12
30
|
### Added
|
|
13
31
|
- **Lock-scoped external attempt authority (spec 8wq.t.34i)**: `AttemptCoordinator#with_verified_attempt` verifies that a managed attempt is live (reserved/running — uncertainty never grants external authority), bound to the exact assignment/project, and owned by the requesting actor, holding the assignment `LifecycleExclusion` shared side across the caller's whole locked transition. `finish` and `reconcile` hold the exclusive side across their terminal commits, so a liveness check can never observe an attempt as active after the terminal event landed (and vice versa). This is the authority behind ace-hitl's managed HITL request binding.
|
|
14
32
|
|
|
15
|
-
|
|
16
33
|
## [0.61.1] - 2026-10-02
|
|
17
34
|
|
|
18
35
|
### Fixed
|
data/README.md
CHANGED
|
@@ -32,7 +32,7 @@ hidden: 66 | done: 64 active: 1 pending: 5 failed: 1
|
|
|
32
32
|
Use `ace-assign status --mode full` when you need the whole tree, and `ace-assign step` when you need the raw instructions for the current or next step.
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
The easiest way to start is through [ace-overseer](../ace-overseer) -- define a task and run `ace-overseer work-on --task <ref> --preset work-on-task` , which creates the assignment, worktree, and
|
|
35
|
+
The easiest way to start is through [ace-overseer](../ace-overseer) -- define a task and run `ace-overseer work-on --task <ref> --preset work-on-task` , which creates the assignment, worktree, and terminal window in one shot.
|
|
36
36
|
|
|
37
37
|
## Testing Contract
|
|
38
38
|
|
data/docs/usage.md
CHANGED
|
@@ -286,28 +286,27 @@ Options:
|
|
|
286
286
|
- `--provider <provider:model>`
|
|
287
287
|
- `--cli-args <args>`
|
|
288
288
|
- `--timeout <seconds>`
|
|
289
|
-
- `--launch-mode auto|headless|tmux`
|
|
289
|
+
- `--launch-mode auto|headless|tmux|herdr`
|
|
290
290
|
- `--callback`
|
|
291
291
|
- `--quiet, -q`
|
|
292
292
|
- `--debug, -d`
|
|
293
293
|
|
|
294
294
|
Launch modes:
|
|
295
295
|
|
|
296
|
-
- `auto` (default): use tmux when the current process is
|
|
297
|
-
- `headless`: force the existing provider subprocess path and never create
|
|
298
|
-
- `tmux`: require tmux context, create or reuse `<origin-window>-fs
|
|
299
|
-
- `
|
|
296
|
+
- `auto` (default): use the detected terminal runtime — tmux when the current process is inside tmux or `ACE_TMUX_SESSION` is set (tmux wins when both are live), herdr when `HERDR_SESSION` + `HERDR_PANE` are live; with no runtime detected, use the headless subprocess path (assign is the only consumer with a headless fallback)
|
|
297
|
+
- `headless`: force the existing provider subprocess path and never create terminal panes
|
|
298
|
+
- `tmux`: require a live tmux context, create or reuse `<origin-window>-fs` through the `ace-runtime` contract, start a real interactive agent in a prepared pane via `ace-llm --interactive`, and send the scoped `/as-assign-drive <assignment>@<root>` handoff automatically
|
|
299
|
+
- `herdr`: the same flow through the Herdr adapter — the fork opens as a tab in the caller's live Herdr workspace and the child inherits `HERDR_*` plus `ACE_RUNTIME=herdr`
|
|
300
300
|
|
|
301
|
-
-
|
|
302
|
-
-
|
|
303
|
-
- Fork windows and panes are created detached, so the current tmux focus stays where the user left it.
|
|
301
|
+
- All terminal targeting, pane dispatch, and capture go through the runtime-neutral `ace-runtime` contract; the fork window name uses the shared `ace-runtime` safe-name policy, so punctuation in the base window is replaced with `-`.
|
|
302
|
+
- Fork windows/panes and herdr tabs are created detached, so the current focus stays where the user left it.
|
|
304
303
|
- Assignment step state remains the source of truth for subtree completion or failure; pane capture is diagnostic support only.
|
|
305
304
|
|
|
306
305
|
Callback mode:
|
|
307
306
|
|
|
308
|
-
- `--callback`:
|
|
309
|
-
- In callback mode the child agent is instructed to send one final status sentence back to the origin pane with `ace-
|
|
310
|
-
- Callback mode is intended for interactive parent/child agent
|
|
307
|
+
- `--callback`: terminal-only fork mode (requires `tmux` or `herdr` launch mode) that captures the pane where `fork-run` was started and passes it into the child fork session as `ACE_ASSIGN_CALLBACK_PANE`
|
|
308
|
+
- In callback mode the child agent is instructed to send one final status sentence back to the origin pane with `ace-runtime send` before stopping
|
|
309
|
+
- Callback mode is intended for interactive parent/child agent flows where the parent stays idle until the child sends the final message back
|
|
311
310
|
|
|
312
311
|
Launch-mode precedence for fork execution:
|
|
313
312
|
|
|
@@ -442,21 +442,21 @@ After launching `ace-assign fork-run`, the driver remains inside the same drive
|
|
|
442
442
|
|
|
443
443
|
When `ACE_ASSIGN_CALLBACK_PANE` is present in the forked child environment:
|
|
444
444
|
|
|
445
|
-
- before stopping for either success or failure, send one final sentence back to the origin pane with direct `ace-
|
|
445
|
+
- before stopping for either success or failure, send one final sentence back to the origin pane with direct `ace-runtime send`
|
|
446
446
|
- use the exact pane target from `ACE_ASSIGN_CALLBACK_PANE`
|
|
447
447
|
- use a success sentence shaped like:
|
|
448
448
|
|
|
449
449
|
```bash
|
|
450
|
-
ace-
|
|
450
|
+
ace-runtime send --pane "$ACE_ASSIGN_CALLBACK_PANE" --msg "Fork subtree ${FORK_ROOT} for assignment ${ASSIGNMENT_ID} completed. Resume parent assignment drive now." --key Enter
|
|
451
451
|
```
|
|
452
452
|
|
|
453
453
|
- use a failure sentence shaped like:
|
|
454
454
|
|
|
455
455
|
```bash
|
|
456
|
-
ace-
|
|
456
|
+
ace-runtime send --pane "$ACE_ASSIGN_CALLBACK_PANE" --msg "Fork subtree ${FORK_ROOT} for assignment ${ASSIGNMENT_ID} failed. Resume parent assignment drive and inspect scoped status." --key Enter
|
|
457
457
|
```
|
|
458
458
|
|
|
459
|
-
- do not invent a new skill or wrapper for this callback; use `ace-
|
|
459
|
+
- do not invent a new skill or wrapper for this callback; use `ace-runtime send` directly
|
|
460
460
|
|
|
461
461
|
#### Post-Fork Resume Checklist
|
|
462
462
|
|
|
@@ -21,8 +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
|
|
25
|
-
option :callback, type: :boolean, default: false, desc: "Capture the origin
|
|
24
|
+
option :launch_mode, desc: "Launch mode: auto, headless, tmux, or herdr"
|
|
25
|
+
option :callback, type: :boolean, default: false, desc: "Capture the origin pane and let the forked agent send a final callback message there"
|
|
26
26
|
option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
|
|
27
27
|
option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
|
|
28
28
|
|
|
@@ -225,13 +225,13 @@ module Ace
|
|
|
225
225
|
def resolve_callback_pane(callback_enabled, launch_mode)
|
|
226
226
|
return nil unless callback_enabled
|
|
227
227
|
|
|
228
|
-
unless launch_mode
|
|
229
|
-
raise Error, "--callback requires
|
|
228
|
+
unless Molecules::ForkSessionLauncher::TERMINAL_LAUNCH_MODES.include?(launch_mode)
|
|
229
|
+
raise Error, "--callback requires a terminal launch mode (tmux or herdr) so the origin pane can be addressed."
|
|
230
230
|
end
|
|
231
231
|
|
|
232
|
-
pane = launcher.respond_to?(:callback_pane) ? launcher.callback_pane : nil
|
|
232
|
+
pane = launcher.respond_to?(:callback_pane) ? launcher.callback_pane(runtime: launch_mode) : nil
|
|
233
233
|
pane = pane.to_s.strip
|
|
234
|
-
raise Error, "--callback requires a resolvable origin
|
|
234
|
+
raise Error, "--callback requires a resolvable origin pane." if pane.empty?
|
|
235
235
|
|
|
236
236
|
pane
|
|
237
237
|
end
|
data/lib/ace/assign/cli.rb
CHANGED
|
@@ -40,7 +40,7 @@ require_relative "molecules/step_writer"
|
|
|
40
40
|
require_relative "molecules/step_renumberer"
|
|
41
41
|
require_relative "molecules/skill_assign_source_resolver"
|
|
42
42
|
require_relative "molecules/fork_session_launcher"
|
|
43
|
-
require_relative "molecules/
|
|
43
|
+
require_relative "molecules/runtime_control_surface_runner"
|
|
44
44
|
require_relative "molecules/preset_inferrer"
|
|
45
45
|
require_relative "molecules/evidence_calculator"
|
|
46
46
|
require_relative "molecules/evidence_journal"
|
|
@@ -11,8 +11,9 @@ module Ace
|
|
|
11
11
|
DEFAULT_PROVIDER = "claude:sonnet"
|
|
12
12
|
DEFAULT_TIMEOUT = 1800
|
|
13
13
|
DEFAULT_LAUNCH_MODE = "auto"
|
|
14
|
-
VALID_LAUNCH_MODES = %w[auto headless tmux].freeze
|
|
15
|
-
|
|
14
|
+
VALID_LAUNCH_MODES = %w[auto headless tmux herdr].freeze
|
|
15
|
+
TERMINAL_LAUNCH_MODES = %w[tmux herdr].freeze
|
|
16
|
+
SUBTREE_POLL_INTERVAL = 0.5
|
|
16
17
|
DEFAULT_TARGET_ENV = "ACE_ASSIGN_DEFAULT_TARGET"
|
|
17
18
|
CURRENT_ASSIGNMENT_ID_ENV = "ACE_ASSIGN_CURRENT_ASSIGNMENT_ID"
|
|
18
19
|
CURRENT_FORK_ROOT_ENV = "ACE_ASSIGN_CURRENT_FORK_ROOT"
|
|
@@ -35,18 +36,11 @@ module Ace
|
|
|
35
36
|
env[CURRENT_FORK_ROOT_ENV].to_s.strip == root_ref
|
|
36
37
|
end
|
|
37
38
|
|
|
38
|
-
def initialize(config: nil, query_interface: Ace::LLM::QueryInterface,
|
|
39
|
-
@config = config || Ace::Assign.config
|
|
40
|
-
@query_interface = query_interface
|
|
41
|
-
@tmux_runner = tmux_runner || TmuxControlSurfaceRunner.new
|
|
42
|
-
@interactive_builder = interactive_builder || Ace::LLM::Molecules::InteractiveCommandBuilder.new
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def initialize(config: nil, query_interface: Ace::LLM::QueryInterface, tmux_runner: nil, interactive_builder: nil,
|
|
39
|
+
def initialize(config: nil, query_interface: Ace::LLM::QueryInterface, runner: nil, interactive_builder: nil,
|
|
46
40
|
lifecycle_exclusion: nil)
|
|
47
41
|
@config = config || Ace::Assign.config
|
|
48
42
|
@query_interface = query_interface
|
|
49
|
-
@
|
|
43
|
+
@runner = runner || RuntimeControlSurfaceRunner.new
|
|
50
44
|
@interactive_builder = interactive_builder || Ace::LLM::Molecules::InteractiveCommandBuilder.new
|
|
51
45
|
@lifecycle_exclusion = lifecycle_exclusion
|
|
52
46
|
end
|
|
@@ -62,31 +56,32 @@ module Ace
|
|
|
62
56
|
# marker fails the launch closed instead of writing into a deleted
|
|
63
57
|
# assignment.
|
|
64
58
|
lifecycle_exclusion.with_shared(lifecycle_exclusion.assignment_key(assignment_id)) do
|
|
65
|
-
if resolved_mode == "
|
|
66
|
-
|
|
59
|
+
if resolved_mode == "headless"
|
|
60
|
+
launch_provider_session(
|
|
67
61
|
assignment_id: assignment_id,
|
|
68
62
|
fork_root: fork_root,
|
|
69
63
|
provider: resolved_provider,
|
|
70
64
|
cli_args: cli_args,
|
|
71
65
|
timeout: resolved_timeout,
|
|
72
|
-
cache_dir: cache_dir
|
|
73
|
-
callback_pane: callback_pane
|
|
66
|
+
cache_dir: cache_dir
|
|
74
67
|
)
|
|
75
68
|
else
|
|
76
|
-
|
|
69
|
+
launch_in_runtime(
|
|
70
|
+
runtime: resolved_mode,
|
|
77
71
|
assignment_id: assignment_id,
|
|
78
72
|
fork_root: fork_root,
|
|
79
73
|
provider: resolved_provider,
|
|
80
74
|
cli_args: cli_args,
|
|
81
75
|
timeout: resolved_timeout,
|
|
82
|
-
cache_dir: cache_dir
|
|
76
|
+
cache_dir: cache_dir,
|
|
77
|
+
callback_pane: callback_pane
|
|
83
78
|
)
|
|
84
79
|
end
|
|
85
80
|
end
|
|
86
81
|
end
|
|
87
82
|
|
|
88
|
-
def callback_pane
|
|
89
|
-
|
|
83
|
+
def callback_pane(runtime: nil)
|
|
84
|
+
runtime_runner_for(runtime).current_pane
|
|
90
85
|
end
|
|
91
86
|
|
|
92
87
|
def launch_provider_session(assignment_id:, fork_root:, provider:, cli_args: nil, timeout: nil, cache_dir: nil,
|
|
@@ -124,7 +119,7 @@ module Ace
|
|
|
124
119
|
|
|
125
120
|
private
|
|
126
121
|
|
|
127
|
-
attr_reader :config, :query_interface, :
|
|
122
|
+
attr_reader :config, :query_interface, :runner, :interactive_builder
|
|
128
123
|
|
|
129
124
|
def lifecycle_exclusion
|
|
130
125
|
@lifecycle_exclusion ||= Molecules::LifecycleExclusion.new
|
|
@@ -140,36 +135,35 @@ module Ace
|
|
|
140
135
|
|
|
141
136
|
return mode unless mode == "auto"
|
|
142
137
|
|
|
143
|
-
|
|
138
|
+
# Assign is the only consumer whose auto mode selects headless,
|
|
139
|
+
# and only when no terminal runtime is detected (k86.3 spec).
|
|
140
|
+
runner.detect_runtime || "headless"
|
|
144
141
|
end
|
|
145
142
|
|
|
146
|
-
def
|
|
143
|
+
def launch_in_runtime(runtime:, assignment_id:, fork_root:, provider:, cli_args:, timeout:, cache_dir:, callback_pane: nil)
|
|
147
144
|
ensure_not_same_scoped_refork!(assignment_id: assignment_id, fork_root: fork_root)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
launch_runner = runtime_runner_for(runtime)
|
|
146
|
+
context = launch_runner.context
|
|
147
|
+
unless context[:in_runtime]
|
|
148
|
+
raise Error, "Launch mode #{runtime} requires a live #{runtime} runtime (run inside it or set its session environment)."
|
|
149
|
+
end
|
|
150
|
+
raise Error, "Runtime launch requires assignment cache_dir for subtree polling." if cache_dir.to_s.strip.empty?
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
raise Error, "Could not resolve current
|
|
152
|
+
origin_window = context[:window]
|
|
153
|
+
raise Error, "Could not resolve current runtime window for fork launch." if origin_window.to_s.strip.empty?
|
|
154
154
|
|
|
155
155
|
fork_window = ENV["ACE_ASSIGN_FORK_WINDOW"].to_s.strip
|
|
156
|
-
fork_window =
|
|
156
|
+
fork_window = launch_runner.fork_window_name(origin_window) if fork_window.empty?
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
pane_target =
|
|
160
|
-
session: session,
|
|
161
|
-
window: fork_window,
|
|
162
|
-
window_target: window_info[:target],
|
|
163
|
-
root: Dir.pwd,
|
|
164
|
-
keep_existing: window_info[:created]
|
|
165
|
-
)
|
|
158
|
+
window = launch_runner.ensure_window(name: fork_window, root: Dir.pwd)
|
|
159
|
+
pane_target = launch_runner.prepare_pane(window: fork_window)
|
|
166
160
|
|
|
167
161
|
session_meta_file = build_session_meta_file(cache_dir, fork_root)
|
|
168
162
|
prompt = "/as-assign-drive #{assignment_id}@#{fork_root}"
|
|
169
|
-
|
|
163
|
+
subprocess_env = runtime_subprocess_env(
|
|
164
|
+
runtime: runtime,
|
|
170
165
|
assignment_id: assignment_id,
|
|
171
166
|
fork_root: fork_root,
|
|
172
|
-
session: session,
|
|
173
167
|
fork_window: fork_window,
|
|
174
168
|
callback_pane: callback_pane
|
|
175
169
|
)
|
|
@@ -178,32 +172,32 @@ module Ace
|
|
|
178
172
|
prompt: prompt,
|
|
179
173
|
cli_args: cli_args,
|
|
180
174
|
working_dir: Dir.pwd,
|
|
181
|
-
subprocess_env:
|
|
175
|
+
subprocess_env: subprocess_env
|
|
182
176
|
)
|
|
183
177
|
|
|
184
|
-
|
|
178
|
+
launch_runner.run_invocation_in_pane(
|
|
185
179
|
pane_target: pane_target,
|
|
186
180
|
command: invocation[:command],
|
|
187
181
|
env: invocation[:env],
|
|
188
182
|
working_dir: invocation[:working_dir],
|
|
189
183
|
visible_handoff: invocation[:prompt]
|
|
190
184
|
)
|
|
191
|
-
|
|
185
|
+
write_runtime_launch_metadata(
|
|
192
186
|
session_meta_file: session_meta_file,
|
|
193
187
|
provider: invocation[:provider],
|
|
194
188
|
model: invocation[:model],
|
|
195
189
|
prompt: invocation[:prompt]
|
|
196
190
|
)
|
|
197
|
-
|
|
191
|
+
launch_runner.merge_runtime_metadata(
|
|
198
192
|
session_meta_file: session_meta_file,
|
|
199
|
-
session: session,
|
|
193
|
+
session: context[:session],
|
|
200
194
|
window: fork_window,
|
|
201
195
|
pane: pane_target,
|
|
202
|
-
window_id:
|
|
196
|
+
window_id: window,
|
|
203
197
|
callback_pane: callback_pane
|
|
204
198
|
)
|
|
205
199
|
|
|
206
|
-
return {
|
|
200
|
+
return {runtime: runtime, pane_target: pane_target, callback_mode: true, callback_pane: callback_pane} if callback_pane
|
|
207
201
|
|
|
208
202
|
wait_for_subtree_terminal(
|
|
209
203
|
assignment_id: assignment_id,
|
|
@@ -212,10 +206,10 @@ module Ace
|
|
|
212
206
|
timeout: timeout
|
|
213
207
|
)
|
|
214
208
|
|
|
215
|
-
{
|
|
209
|
+
{runtime: runtime, pane_target: pane_target}
|
|
216
210
|
end
|
|
217
211
|
|
|
218
|
-
def
|
|
212
|
+
def write_runtime_launch_metadata(session_meta_file:, provider:, model:, prompt:)
|
|
219
213
|
detected = detect_provider_session(provider, prompt)
|
|
220
214
|
meta = {}
|
|
221
215
|
meta = YAML.safe_load_file(session_meta_file) || {} if File.exist?(session_meta_file)
|
|
@@ -278,11 +272,19 @@ module Ace
|
|
|
278
272
|
"Cannot fork-run subtree #{assignment_id}@#{fork_root}: already running inside that scoped subtree. Continue inline instead of calling fork-run again."
|
|
279
273
|
end
|
|
280
274
|
|
|
281
|
-
def
|
|
275
|
+
def runtime_runner_for(runtime)
|
|
276
|
+
if runtime == runner.runtime_name
|
|
277
|
+
runner
|
|
278
|
+
else
|
|
279
|
+
RuntimeControlSurfaceRunner.new(runtime: runtime)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def runtime_subprocess_env(runtime:, assignment_id:, fork_root:, fork_window:, callback_pane: nil)
|
|
282
284
|
env = self.class.fork_scope_env(assignment_id: assignment_id, fork_root: fork_root).merge(
|
|
283
285
|
"PROJECT_ROOT_PATH" => Dir.pwd,
|
|
284
|
-
"
|
|
285
|
-
"ACE_ASSIGN_LAUNCH_MODE" =>
|
|
286
|
+
"ACE_RUNTIME" => runtime,
|
|
287
|
+
"ACE_ASSIGN_LAUNCH_MODE" => runtime,
|
|
286
288
|
"ACE_ASSIGN_FORK_WINDOW" => fork_window
|
|
287
289
|
)
|
|
288
290
|
env["ACE_ASSIGN_CALLBACK_PANE"] = callback_pane if callback_pane && !callback_pane.empty?
|
|
@@ -303,13 +305,13 @@ module Ace
|
|
|
303
305
|
loop do
|
|
304
306
|
state = scanner.scan(assignment.steps_dir, assignment: assignment)
|
|
305
307
|
return state if state.subtree_complete?(fork_root)
|
|
306
|
-
raise Error, "Fork session execution failed in
|
|
308
|
+
raise Error, "Fork session execution failed in the #{fork_root} subtree." if state.subtree_failed?(fork_root)
|
|
307
309
|
|
|
308
310
|
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
309
|
-
raise Error, "Timed out waiting for
|
|
311
|
+
raise Error, "Timed out waiting for the fork subtree #{fork_root} to reach a terminal state."
|
|
310
312
|
end
|
|
311
313
|
|
|
312
|
-
sleep(
|
|
314
|
+
sleep(SUBTREE_POLL_INTERVAL)
|
|
313
315
|
end
|
|
314
316
|
end
|
|
315
317
|
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/runtime"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "shellwords"
|
|
6
|
+
require "yaml"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Assign
|
|
10
|
+
module Molecules
|
|
11
|
+
# Contract-backed terminal helper for fork launches. Every terminal
|
|
12
|
+
# operation delegates to the resolved Ace::Runtime adapter; assign
|
|
13
|
+
# never calls a runtime-native API directly (k86.3 interface rule).
|
|
14
|
+
class RuntimeControlSurfaceRunner
|
|
15
|
+
def initialize(runtime: nil, env: ENV)
|
|
16
|
+
@runtime_name = runtime
|
|
17
|
+
@env = env
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :runtime_name, :env
|
|
21
|
+
|
|
22
|
+
def runtime
|
|
23
|
+
@runtime ||= Ace::Runtime.resolve(runtime_name || selected_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def context
|
|
27
|
+
runtime.context
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Side-effect-free runtime detection ("tmux", "herdr", or nil) for
|
|
31
|
+
# the assign auto launch mode.
|
|
32
|
+
def detect_runtime
|
|
33
|
+
detected = Ace::Runtime.detect(env: env)
|
|
34
|
+
detected&.to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def in_runtime?
|
|
38
|
+
context[:in_runtime] ? true : false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def current_session
|
|
42
|
+
context[:session]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def current_pane
|
|
46
|
+
explicit = env["ACE_ASSIGN_CALLBACK_PANE"].to_s.strip
|
|
47
|
+
return explicit unless explicit.empty?
|
|
48
|
+
|
|
49
|
+
context[:pane]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fork_window_name(base_window)
|
|
53
|
+
base = base_window.to_s.strip.sub(/-fs\z/, "")
|
|
54
|
+
|
|
55
|
+
"#{Ace::Runtime.sanitize_name(base, fallback: "fork")}-fs"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ensure_window(name:, root:)
|
|
59
|
+
runtime.ensure_window(name: name, root: root)
|
|
60
|
+
rescue Ace::Runtime::Error => e
|
|
61
|
+
raise Error, "Failed to ensure fork window #{name}: #{e.message}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def prepare_pane(window:)
|
|
65
|
+
runtime.prepare_pane(window: window)
|
|
66
|
+
rescue Ace::Runtime::Error => e
|
|
67
|
+
raise Error, "Failed to prepare fork pane in #{window}: #{e.message}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def run_invocation_in_pane(pane_target:, command:, env: nil, working_dir: nil, visible_handoff: nil)
|
|
71
|
+
shell_command = build_pane_shell_command(
|
|
72
|
+
command: command,
|
|
73
|
+
env: env,
|
|
74
|
+
working_dir: working_dir,
|
|
75
|
+
visible_handoff: visible_handoff
|
|
76
|
+
)
|
|
77
|
+
runtime.send_command(pane: pane_target, command: shell_command)
|
|
78
|
+
rescue Ace::Runtime::Error => e
|
|
79
|
+
raise Error, "Failed to send fork command: #{e.message}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def run_script_in_pane(pane_target:, script_path:)
|
|
83
|
+
runtime.send_command(pane: pane_target, command: "bash #{File.expand_path(script_path).shellescape}")
|
|
84
|
+
rescue Ace::Runtime::Error => e
|
|
85
|
+
raise Error, "Failed to send fork command: #{e.message}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def capture_recent_output(pane_target:, lines: 40)
|
|
89
|
+
runtime.capture(pane: pane_target, lines: lines)
|
|
90
|
+
rescue Ace::Runtime::Error => e
|
|
91
|
+
raise Error, "Failed to capture pane #{pane_target}: #{e.message}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def merge_runtime_metadata(session_meta_file:, session:, window:, pane:, window_id: nil, callback_pane: nil)
|
|
95
|
+
data = if File.exist?(session_meta_file)
|
|
96
|
+
YAML.safe_load_file(session_meta_file) || {}
|
|
97
|
+
else
|
|
98
|
+
{}
|
|
99
|
+
end
|
|
100
|
+
data["launch_mode"] = runtime_name
|
|
101
|
+
data["runtime"] = runtime_name
|
|
102
|
+
data["session"] = session
|
|
103
|
+
data["window"] = window
|
|
104
|
+
data["window_id"] = window_id if window_id
|
|
105
|
+
data["pane"] = pane
|
|
106
|
+
data["callback_pane"] = callback_pane if callback_pane && !callback_pane.empty?
|
|
107
|
+
File.write(session_meta_file, data.to_yaml)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def selected_name
|
|
113
|
+
detected = Ace::Runtime.detect(env: env)
|
|
114
|
+
return detected.to_s if detected
|
|
115
|
+
|
|
116
|
+
raise Error, "no terminal runtime detected: run inside tmux/herdr, set ACE_RUNTIME, or pass an explicit runtime"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def build_pane_shell_command(command:, env:, working_dir:, visible_handoff:)
|
|
120
|
+
steps = []
|
|
121
|
+
resolved_working_dir = working_dir.to_s.strip
|
|
122
|
+
steps << "cd #{Shellwords.escape(File.expand_path(resolved_working_dir))}" unless resolved_working_dir.empty?
|
|
123
|
+
|
|
124
|
+
handoff = visible_handoff.to_s
|
|
125
|
+
steps << "printf '%s\\n' #{Shellwords.escape(handoff)}" unless handoff.empty?
|
|
126
|
+
|
|
127
|
+
# The prepared pane owns a retained interactive shell. Run the
|
|
128
|
+
# invocation as its child so another submission can use the same
|
|
129
|
+
# target after this command exits.
|
|
130
|
+
steps << build_exec_command(command: command, env: env)
|
|
131
|
+
steps.join(" && ")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def build_exec_command(command:, env:)
|
|
135
|
+
cmd = Array(command).map { |part| Shellwords.escape(part.to_s) }.join(" ")
|
|
136
|
+
env_hash = env.respond_to?(:to_h) ? env.to_h : {}
|
|
137
|
+
unset_parts = []
|
|
138
|
+
assign_parts = []
|
|
139
|
+
|
|
140
|
+
env_hash.each do |key, value|
|
|
141
|
+
next if key.to_s.strip.empty?
|
|
142
|
+
|
|
143
|
+
if value.nil?
|
|
144
|
+
unset_parts << "-u #{Shellwords.escape(key.to_s)}"
|
|
145
|
+
else
|
|
146
|
+
assign_parts << "#{key}=#{Shellwords.escape(value.to_s)}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
env_parts = unset_parts + assign_parts
|
|
150
|
+
|
|
151
|
+
return cmd if env_parts.empty?
|
|
152
|
+
|
|
153
|
+
"env #{env_parts.join(' ')} #{cmd}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/ace/assign/version.rb
CHANGED
data/lib/ace/assign.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-assign
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.63.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
@@ -135,6 +135,20 @@ dependencies:
|
|
|
135
135
|
- - "~>"
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
137
|
version: '0.37'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: ace-runtime
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0.1'
|
|
145
|
+
type: :runtime
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - "~>"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '0.1'
|
|
138
152
|
- !ruby/object:Gem::Dependency
|
|
139
153
|
name: ace-tmux
|
|
140
154
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -149,6 +163,20 @@ dependencies:
|
|
|
149
163
|
- - "~>"
|
|
150
164
|
- !ruby/object:Gem::Version
|
|
151
165
|
version: '0.18'
|
|
166
|
+
- !ruby/object:Gem::Dependency
|
|
167
|
+
name: ace-herdr
|
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
|
169
|
+
requirements:
|
|
170
|
+
- - "~>"
|
|
171
|
+
- !ruby/object:Gem::Version
|
|
172
|
+
version: 0.3.2
|
|
173
|
+
type: :runtime
|
|
174
|
+
prerelease: false
|
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - "~>"
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: 0.3.2
|
|
152
180
|
- !ruby/object:Gem::Dependency
|
|
153
181
|
name: ace-support-test-helpers
|
|
154
182
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -344,10 +372,10 @@ files:
|
|
|
344
372
|
- lib/ace/assign/molecules/preset_inferrer.rb
|
|
345
373
|
- lib/ace/assign/molecules/queue_scanner.rb
|
|
346
374
|
- lib/ace/assign/molecules/receipt_verifier.rb
|
|
375
|
+
- lib/ace/assign/molecules/runtime_control_surface_runner.rb
|
|
347
376
|
- lib/ace/assign/molecules/skill_assign_source_resolver.rb
|
|
348
377
|
- lib/ace/assign/molecules/step_renumberer.rb
|
|
349
378
|
- lib/ace/assign/molecules/step_writer.rb
|
|
350
|
-
- lib/ace/assign/molecules/tmux_control_surface_runner.rb
|
|
351
379
|
- lib/ace/assign/organisms/assignment_executor.rb
|
|
352
380
|
- lib/ace/assign/organisms/attempt_coordinator.rb
|
|
353
381
|
- lib/ace/assign/organisms/task_assignment_creator.rb
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ace/tmux"
|
|
4
|
-
require "fileutils"
|
|
5
|
-
require "shellwords"
|
|
6
|
-
require "yaml"
|
|
7
|
-
|
|
8
|
-
module Ace
|
|
9
|
-
module Assign
|
|
10
|
-
module Molecules
|
|
11
|
-
# Shared ace-tmux backed runtime helper for tmux fork launches.
|
|
12
|
-
class TmuxControlSurfaceRunner
|
|
13
|
-
def initialize(executor: Ace::Tmux::Molecules::TmuxExecutor.new, resolver: nil, control_surface: nil, tmux: "tmux", env: ENV)
|
|
14
|
-
@executor = executor
|
|
15
|
-
@tmux = tmux
|
|
16
|
-
@env = env
|
|
17
|
-
@resolver = resolver || Ace::Tmux::Molecules::RuntimeTargetResolver.new(executor: executor, tmux: tmux, env: env)
|
|
18
|
-
@control_surface = control_surface || Ace::Tmux::Organisms::ControlSurface.new(
|
|
19
|
-
executor: executor,
|
|
20
|
-
resolver: @resolver,
|
|
21
|
-
tmux: tmux
|
|
22
|
-
)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def tmux_context?
|
|
26
|
-
!current_session.to_s.strip.empty?
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def current_session
|
|
30
|
-
resolver.resolve_session.session
|
|
31
|
-
rescue Ace::Tmux::TargetResolutionError
|
|
32
|
-
nil
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def current_window
|
|
36
|
-
explicit = env["ACE_ASSIGN_FORK_WINDOW"].to_s.strip
|
|
37
|
-
return explicit unless explicit.empty?
|
|
38
|
-
|
|
39
|
-
resolver.resolve_window(session: current_session).window
|
|
40
|
-
rescue Ace::Tmux::TargetResolutionError
|
|
41
|
-
nil
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def current_pane
|
|
45
|
-
explicit = env["ACE_ASSIGN_CALLBACK_PANE"].to_s.strip
|
|
46
|
-
return explicit unless explicit.empty?
|
|
47
|
-
|
|
48
|
-
resolver.resolve_pane(session: current_session, window: current_window).pane_target
|
|
49
|
-
rescue Ace::Tmux::TargetResolutionError
|
|
50
|
-
nil
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def fork_window_name(base_window)
|
|
54
|
-
base = base_window.to_s.strip.sub(/-fs\z/, "")
|
|
55
|
-
sanitized = Ace::Tmux::Atoms::WindowNameSanitizer.call(base, fallback: "fork")
|
|
56
|
-
|
|
57
|
-
"#{sanitized}-fs"
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def ensure_window(session:, name:, root:)
|
|
61
|
-
if (window_id = find_window_id(session: session, name: name))
|
|
62
|
-
return {created: false, target: window_id, window_id: window_id, name: name}
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
result = executor.capture(
|
|
66
|
-
Ace::Tmux::Atoms::TmuxCommandBuilder.new_window(
|
|
67
|
-
session,
|
|
68
|
-
name: name,
|
|
69
|
-
root: root,
|
|
70
|
-
print_format: '#{window_id}',
|
|
71
|
-
tmux: tmux
|
|
72
|
-
)
|
|
73
|
-
)
|
|
74
|
-
raise Error, "Failed to create tmux fork window #{name}: #{result.stderr}" unless result.success?
|
|
75
|
-
|
|
76
|
-
window_id = result.stdout.to_s.strip
|
|
77
|
-
raise Error, "Failed to create tmux fork window #{name}: empty window id" if window_id.empty?
|
|
78
|
-
|
|
79
|
-
{created: true, target: window_id, window_id: window_id, name: name}
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def prepare_pane(session:, window:, root:, keep_existing:, window_target: nil)
|
|
83
|
-
target = window_target || "#{session}:#{window}"
|
|
84
|
-
pane = if keep_existing
|
|
85
|
-
first_pane(target)
|
|
86
|
-
else
|
|
87
|
-
create_pane(target, root)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
set_pane_remain_on_exit(pane)
|
|
91
|
-
select_layout(target)
|
|
92
|
-
pane
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def select_window(session:, window:, window_target: nil)
|
|
96
|
-
target = window_target || "#{session}:#{window}"
|
|
97
|
-
run!(
|
|
98
|
-
Ace::Tmux::Atoms::TmuxCommandBuilder.select_window(target, tmux: tmux),
|
|
99
|
-
"select tmux fork window #{window}"
|
|
100
|
-
)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def run_invocation_in_pane(pane_target:, command:, env: nil, working_dir: nil, visible_handoff: nil)
|
|
104
|
-
shell_command = build_pane_shell_command(
|
|
105
|
-
command: command,
|
|
106
|
-
env: env,
|
|
107
|
-
working_dir: working_dir,
|
|
108
|
-
visible_handoff: visible_handoff
|
|
109
|
-
)
|
|
110
|
-
control_surface.send_command(pane: pane_target, command: shell_command)
|
|
111
|
-
rescue Ace::Tmux::Error => e
|
|
112
|
-
raise Error, "Failed to send tmux fork command: #{e.message}"
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def run_script_in_pane(pane_target:, script_path:)
|
|
116
|
-
control_surface.send_command(pane: pane_target, command: "bash #{File.expand_path(script_path).shellescape}")
|
|
117
|
-
rescue Ace::Tmux::Error => e
|
|
118
|
-
raise Error, "Failed to send tmux fork command: #{e.message}"
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def capture_recent_output(pane_target:, lines: 40)
|
|
122
|
-
control_surface.capture_recent_output(pane: pane_target, lines: lines)
|
|
123
|
-
rescue Ace::Tmux::Error => e
|
|
124
|
-
raise Error, "Failed to capture pane #{pane_target}: #{e.message}"
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def merge_tmux_metadata(session_meta_file:, session:, window:, pane:, window_id: nil, callback_pane: nil)
|
|
128
|
-
data = if File.exist?(session_meta_file)
|
|
129
|
-
YAML.safe_load_file(session_meta_file) || {}
|
|
130
|
-
else
|
|
131
|
-
{}
|
|
132
|
-
end
|
|
133
|
-
data["launch_mode"] = "tmux"
|
|
134
|
-
data["tmux_session"] = session
|
|
135
|
-
data["tmux_window"] = window
|
|
136
|
-
data["tmux_window_id"] = window_id if window_id
|
|
137
|
-
data["tmux_pane_id"] = pane
|
|
138
|
-
data["callback_pane"] = callback_pane if callback_pane && !callback_pane.empty?
|
|
139
|
-
File.write(session_meta_file, data.to_yaml)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
private
|
|
143
|
-
|
|
144
|
-
attr_reader :control_surface, :env, :executor, :resolver, :tmux
|
|
145
|
-
|
|
146
|
-
def find_window_id(session:, name:)
|
|
147
|
-
result = executor.capture(
|
|
148
|
-
Ace::Tmux::Atoms::TmuxCommandBuilder.list_windows(
|
|
149
|
-
session,
|
|
150
|
-
format: '#{window_id}' + "\t" + '#{window_name}',
|
|
151
|
-
tmux: tmux
|
|
152
|
-
)
|
|
153
|
-
)
|
|
154
|
-
return nil unless result.success?
|
|
155
|
-
|
|
156
|
-
result.stdout.split("\n").map(&:strip).reject(&:empty?).each do |line|
|
|
157
|
-
window_id, window_name = line.split("\t", 2)
|
|
158
|
-
return window_id if window_name == name && !window_id.to_s.empty?
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
nil
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def first_pane(target)
|
|
165
|
-
result = executor.capture(
|
|
166
|
-
Ace::Tmux::Atoms::TmuxCommandBuilder.list_panes(target, format: '#{pane_id}', tmux: tmux)
|
|
167
|
-
)
|
|
168
|
-
raise Error, "Failed to inspect panes for #{target}: #{result.stderr}" unless result.success?
|
|
169
|
-
|
|
170
|
-
pane = result.stdout.split("\n").map(&:strip).reject(&:empty?).first
|
|
171
|
-
raise Error, "No panes found for #{target}" if pane.to_s.empty?
|
|
172
|
-
|
|
173
|
-
pane
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def create_pane(target, root)
|
|
177
|
-
result = executor.capture(
|
|
178
|
-
Ace::Tmux::Atoms::TmuxCommandBuilder.split_window(
|
|
179
|
-
target,
|
|
180
|
-
root: root,
|
|
181
|
-
print_format: '#{pane_id}',
|
|
182
|
-
tmux: tmux
|
|
183
|
-
)
|
|
184
|
-
)
|
|
185
|
-
raise Error, "Failed to create tmux fork pane in #{target}: #{result.stderr}" unless result.success?
|
|
186
|
-
|
|
187
|
-
pane = result.stdout.to_s.strip
|
|
188
|
-
raise Error, "Failed to create tmux fork pane in #{target}: empty pane id" if pane.empty?
|
|
189
|
-
|
|
190
|
-
pane
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def set_pane_remain_on_exit(pane_target)
|
|
194
|
-
run!(
|
|
195
|
-
Ace::Tmux::Atoms::TmuxCommandBuilder.set_pane_option(pane_target, "remain-on-exit", "on", tmux: tmux),
|
|
196
|
-
"enable remain-on-exit"
|
|
197
|
-
)
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def select_layout(target)
|
|
201
|
-
run!(
|
|
202
|
-
Ace::Tmux::Atoms::TmuxCommandBuilder.select_layout(target, "tiled", tmux: tmux),
|
|
203
|
-
"apply tiled layout"
|
|
204
|
-
)
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def run!(cmd, action)
|
|
208
|
-
return if executor.run(cmd)
|
|
209
|
-
|
|
210
|
-
raise Error, "Failed to #{action}"
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def build_pane_shell_command(command:, env:, working_dir:, visible_handoff:)
|
|
214
|
-
steps = []
|
|
215
|
-
resolved_working_dir = working_dir.to_s.strip
|
|
216
|
-
steps << "cd #{Shellwords.escape(File.expand_path(resolved_working_dir))}" unless resolved_working_dir.empty?
|
|
217
|
-
|
|
218
|
-
handoff = visible_handoff.to_s
|
|
219
|
-
steps << "printf '%s\\n' #{Shellwords.escape(handoff)}" unless handoff.empty?
|
|
220
|
-
|
|
221
|
-
steps << "exec #{build_exec_command(command: command, env: env)}"
|
|
222
|
-
steps.join(" && ")
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def build_exec_command(command:, env:)
|
|
226
|
-
cmd = Array(command).map { |part| Shellwords.escape(part.to_s) }.join(" ")
|
|
227
|
-
env_hash = env.respond_to?(:to_h) ? env.to_h : {}
|
|
228
|
-
unset_parts = []
|
|
229
|
-
assign_parts = []
|
|
230
|
-
|
|
231
|
-
env_hash.each do |key, value|
|
|
232
|
-
next if key.to_s.strip.empty?
|
|
233
|
-
|
|
234
|
-
if value.nil?
|
|
235
|
-
unset_parts << "-u #{Shellwords.escape(key.to_s)}"
|
|
236
|
-
else
|
|
237
|
-
assign_parts << "#{key}=#{Shellwords.escape(value.to_s)}"
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
env_parts = unset_parts + assign_parts
|
|
241
|
-
|
|
242
|
-
return cmd if env_parts.empty?
|
|
243
|
-
|
|
244
|
-
"env #{env_parts.join(' ')} #{cmd}"
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
end
|