harnex 0.6.5 → 0.7.3
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/CHANGELOG.md +138 -0
- data/README.md +71 -21
- data/TECHNICAL.md +23 -0
- data/guides/01_dispatch.md +2 -0
- data/lib/harnex/adapters/base.rb +33 -0
- data/lib/harnex/adapters/claude.rb +4 -0
- data/lib/harnex/adapters/codex.rb +4 -0
- data/lib/harnex/adapters/codex_appserver.rb +56 -230
- data/lib/harnex/adapters/opencode.rb +132 -0
- data/lib/harnex/adapters.rb +3 -1
- data/lib/harnex/cli.rb +8 -1
- data/lib/harnex/codex/app_server/client.rb +348 -0
- data/lib/harnex/commands/doctor.rb +95 -2
- data/lib/harnex/commands/history.rb +149 -0
- data/lib/harnex/commands/run.rb +44 -6
- data/lib/harnex/commands/wait.rb +77 -36
- data/lib/harnex/core.rb +3 -3
- data/lib/harnex/dispatch_history.rb +112 -0
- data/lib/harnex/runtime/session.rb +164 -23
- data/lib/harnex/version.rb +2 -2
- data/lib/harnex.rb +2 -0
- metadata +6 -2
data/lib/harnex/commands/run.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Harnex
|
|
|
8
8
|
KNOWN_FLAGS = %w[
|
|
9
9
|
--id --description --detach --tmux --host --port --watch --watch-file
|
|
10
10
|
--stall-after --max-resumes --preset --context --meta --summary-out
|
|
11
|
-
--timeout --inbox-ttl --auto-stop --legacy-pty --help
|
|
11
|
+
--timeout --inbox-ttl --auto-stop --fast --legacy-pty --help
|
|
12
12
|
].freeze
|
|
13
13
|
VALUE_FLAGS = %w[
|
|
14
14
|
--id --description --host --port --watch --watch-file --stall-after
|
|
@@ -33,6 +33,8 @@ module Harnex
|
|
|
33
33
|
--watch-file PATH Auto-send a file-change hook on modification
|
|
34
34
|
--context TEXT Inject as the initial prompt (prepends session header)
|
|
35
35
|
--auto-stop Stop after the first task completion from --context
|
|
36
|
+
--fast (codex only) Use Codex service_tier="fast".
|
|
37
|
+
Default Codex runs force service_tier="flex".
|
|
36
38
|
--meta JSON Attach parsed JSON metadata to the started event
|
|
37
39
|
--summary-out PATH Append dispatch telemetry summary JSONL to PATH
|
|
38
40
|
--timeout SECS Max seconds to wait for detached registration (default: #{DEFAULT_TIMEOUT})
|
|
@@ -70,6 +72,7 @@ module Harnex
|
|
|
70
72
|
|
|
71
73
|
def initialize(argv)
|
|
72
74
|
@argv = argv.dup
|
|
75
|
+
@launch_cwd = Dir.pwd
|
|
73
76
|
@options = {
|
|
74
77
|
id: nil,
|
|
75
78
|
description: nil,
|
|
@@ -91,6 +94,7 @@ module Harnex
|
|
|
91
94
|
tmux_name: nil,
|
|
92
95
|
timeout: DEFAULT_TIMEOUT,
|
|
93
96
|
inbox_ttl: default_inbox_ttl,
|
|
97
|
+
fast: false,
|
|
94
98
|
legacy_pty: false,
|
|
95
99
|
help: false
|
|
96
100
|
}
|
|
@@ -110,7 +114,7 @@ module Harnex
|
|
|
110
114
|
@options[:summary_out] = resolve_summary_out(repo_root)
|
|
111
115
|
@options[:id] ||= Harnex.generate_id(repo_root)
|
|
112
116
|
validate_unique_id!(repo_root)
|
|
113
|
-
effective_child_args = apply_context(child_args)
|
|
117
|
+
effective_child_args = apply_context(apply_codex_service_tier(cli_name, child_args))
|
|
114
118
|
adapter = Harnex.build_adapter(cli_name, effective_child_args, legacy_pty: @options[:legacy_pty])
|
|
115
119
|
@options[:detach] = true if @options[:tmux]
|
|
116
120
|
validate_watch_mode!
|
|
@@ -170,6 +174,7 @@ module Harnex
|
|
|
170
174
|
tmux_cmd += ["--meta", JSON.generate(@options[:meta])] if @options[:meta]
|
|
171
175
|
tmux_cmd += ["--summary-out", @options[:summary_out]] if @options[:summary_out]
|
|
172
176
|
tmux_cmd += ["--inbox-ttl", @options[:inbox_ttl].to_s]
|
|
177
|
+
tmux_cmd << "--fast" if @options[:fast]
|
|
173
178
|
tmux_cmd += ["--legacy-pty"] if @options[:legacy_pty]
|
|
174
179
|
tmux_cmd += ["--"] + child_args unless child_args.empty?
|
|
175
180
|
|
|
@@ -178,9 +183,9 @@ module Harnex
|
|
|
178
183
|
|
|
179
184
|
started =
|
|
180
185
|
if ENV["TMUX"]
|
|
181
|
-
system("tmux", "new-window", "-n", window_name, "-d", shell_cmd)
|
|
186
|
+
system("tmux", "new-window", "-c", @launch_cwd, "-n", window_name, "-d", shell_cmd)
|
|
182
187
|
else
|
|
183
|
-
system("tmux", "new-session", "-d", "-s", "harnex", "-n", window_name, shell_cmd)
|
|
188
|
+
system("tmux", "new-session", "-c", @launch_cwd, "-d", "-s", "harnex", "-n", window_name, shell_cmd)
|
|
184
189
|
end
|
|
185
190
|
|
|
186
191
|
raise "tmux failed to start #{cli_name.inspect}" unless started
|
|
@@ -275,7 +280,8 @@ module Harnex
|
|
|
275
280
|
meta: @options[:meta],
|
|
276
281
|
summary_out: @options[:summary_out],
|
|
277
282
|
inbox_ttl: @options[:inbox_ttl],
|
|
278
|
-
auto_stop: @options[:auto_stop]
|
|
283
|
+
auto_stop: @options[:auto_stop],
|
|
284
|
+
launch_cwd: @launch_cwd
|
|
279
285
|
)
|
|
280
286
|
end
|
|
281
287
|
|
|
@@ -423,6 +429,8 @@ module Harnex
|
|
|
423
429
|
@options[:context] = required_option_value("--context", Regexp.last_match(1))
|
|
424
430
|
when "--auto-stop"
|
|
425
431
|
@options[:auto_stop] = true
|
|
432
|
+
when "--fast"
|
|
433
|
+
@options[:fast] = true
|
|
426
434
|
when "--meta"
|
|
427
435
|
index += 1
|
|
428
436
|
@options[:meta] = parse_meta(required_option_value(arg, argv[index]))
|
|
@@ -446,6 +454,7 @@ module Harnex
|
|
|
446
454
|
when "--legacy-pty"
|
|
447
455
|
@options[:legacy_pty] = true
|
|
448
456
|
else
|
|
457
|
+
reject_unknown_long_flag!(arg) if unknown_long_flag?(arg)
|
|
449
458
|
if cli_name.nil?
|
|
450
459
|
cli_name = arg
|
|
451
460
|
else
|
|
@@ -458,6 +467,16 @@ module Harnex
|
|
|
458
467
|
[cli_name, forwarded]
|
|
459
468
|
end
|
|
460
469
|
|
|
470
|
+
def unknown_long_flag?(arg)
|
|
471
|
+
arg.start_with?("--")
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def reject_unknown_long_flag!(arg)
|
|
475
|
+
flag = arg.split("=", 2).first
|
|
476
|
+
raise OptionParser::InvalidOption,
|
|
477
|
+
"harnex run: unknown flag #{flag}; see harnex run --help"
|
|
478
|
+
end
|
|
479
|
+
|
|
461
480
|
def required_option_value(option_name, value)
|
|
462
481
|
raise OptionParser::MissingArgument, option_name if value.nil?
|
|
463
482
|
raise OptionParser::MissingArgument, option_name if value.match?(/\A-[A-Za-z]/)
|
|
@@ -484,7 +503,7 @@ module Harnex
|
|
|
484
503
|
case arg
|
|
485
504
|
when "--"
|
|
486
505
|
return false
|
|
487
|
-
when "-h", "--help", "--detach", "--tmux", "--auto-stop", "--legacy-pty"
|
|
506
|
+
when "-h", "--help", "--detach", "--tmux", "--auto-stop", "--fast", "--legacy-pty"
|
|
488
507
|
nil
|
|
489
508
|
when /\A--tmux=/
|
|
490
509
|
nil
|
|
@@ -538,6 +557,25 @@ module Harnex
|
|
|
538
557
|
raise OptionParser::InvalidOption, "harnex run: --auto-stop requires --context"
|
|
539
558
|
end
|
|
540
559
|
|
|
560
|
+
def apply_codex_service_tier(cli_name, child_args)
|
|
561
|
+
return child_args unless cli_name.to_s == "codex"
|
|
562
|
+
return child_args if child_service_tier_config?(child_args)
|
|
563
|
+
|
|
564
|
+
child_args + ["-c", "service_tier=\"#{@options[:fast] ? 'fast' : 'flex'}\""]
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def child_service_tier_config?(child_args)
|
|
568
|
+
child_args.each_with_index.any? do |arg, index|
|
|
569
|
+
text = arg.to_s
|
|
570
|
+
text.start_with?("service_tier=") ||
|
|
571
|
+
text.start_with?("service_tier.") ||
|
|
572
|
+
text == "-c" && child_args[index + 1].to_s.start_with?("service_tier") ||
|
|
573
|
+
text == "--config" && child_args[index + 1].to_s.start_with?("service_tier") ||
|
|
574
|
+
text.start_with?("-cservice_tier") ||
|
|
575
|
+
text.start_with?("--config=service_tier")
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
541
579
|
def parse_non_negative_integer(value, option_name:)
|
|
542
580
|
integer = Integer(value)
|
|
543
581
|
raise OptionParser::InvalidArgument, "#{option_name} must be 0 or greater" if integer.negative?
|
data/lib/harnex/commands/wait.rb
CHANGED
|
@@ -6,8 +6,13 @@ require "uri"
|
|
|
6
6
|
module Harnex
|
|
7
7
|
class Waiter
|
|
8
8
|
POLL_INTERVAL = 0.5
|
|
9
|
+
EVENT_POLL_INTERVAL = 0.1
|
|
10
|
+
EXIT_STATUS_GRACE_SECONDS_DEFAULT = 5.0
|
|
11
|
+
EXIT_STATUS_GRACE_POLL_INTERVAL = 0.05
|
|
12
|
+
FINAL_EVENT_GRACE_SECONDS = 5.0
|
|
9
13
|
|
|
10
14
|
EVENT_PREDICATES = %w[task_complete].freeze
|
|
15
|
+
LEGACY_EVENT_TYPES = %w[agent_state exited task_complete].freeze
|
|
11
16
|
|
|
12
17
|
def self.usage(program_name = "harnex wait")
|
|
13
18
|
<<~TEXT
|
|
@@ -84,30 +89,17 @@ module Harnex
|
|
|
84
89
|
|
|
85
90
|
offset = 0
|
|
86
91
|
task_complete_seen = false
|
|
92
|
+
final_event_deadline = nil
|
|
87
93
|
|
|
88
94
|
# Replay existing events first — we may already be past the predicate.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
f.each_line do |line|
|
|
92
|
-
offset = f.pos
|
|
93
|
-
event = parse_event(line)
|
|
94
|
-
next unless event
|
|
95
|
-
task_complete_seen = true if event["type"] == "task_complete"
|
|
96
|
-
if matches?(event, predicate, task_complete_seen)
|
|
97
|
-
return emit_event_match(event, start_time)
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
95
|
+
status, offset, task_complete_seen = scan_events(events_path, offset, predicate, task_complete_seen, start_time)
|
|
96
|
+
return status if status
|
|
102
97
|
|
|
103
98
|
target_pid = registry && registry["pid"]
|
|
104
99
|
|
|
105
100
|
loop do
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
puts JSON.generate(ok: false, id: @options[:id], state: "exited", waited_seconds: waited)
|
|
109
|
-
return 1
|
|
110
|
-
end
|
|
101
|
+
status, offset, task_complete_seen = scan_events(events_path, offset, predicate, task_complete_seen, start_time)
|
|
102
|
+
return status if status
|
|
111
103
|
|
|
112
104
|
if deadline && Time.now >= deadline
|
|
113
105
|
waited = (Time.now - start_time).round(1)
|
|
@@ -115,34 +107,62 @@ module Harnex
|
|
|
115
107
|
return 124
|
|
116
108
|
end
|
|
117
109
|
|
|
118
|
-
if
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
task_complete_seen = true if event["type"] == "task_complete"
|
|
125
|
-
if matches?(event, predicate, task_complete_seen)
|
|
126
|
-
offset = f.pos
|
|
127
|
-
return emit_event_match(event, start_time)
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
offset = f.pos
|
|
110
|
+
if target_pid && !Harnex.alive_pid?(target_pid)
|
|
111
|
+
final_event_deadline ||= Time.now + FINAL_EVENT_GRACE_SECONDS
|
|
112
|
+
if Time.now >= final_event_deadline
|
|
113
|
+
waited = (Time.now - start_time).round(1)
|
|
114
|
+
puts JSON.generate(ok: false, id: @options[:id], state: "exited", waited_seconds: waited)
|
|
115
|
+
return 1
|
|
131
116
|
end
|
|
117
|
+
else
|
|
118
|
+
final_event_deadline = nil
|
|
132
119
|
end
|
|
133
120
|
|
|
134
|
-
sleep
|
|
121
|
+
sleep EVENT_POLL_INTERVAL
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def scan_events(path, offset, predicate, task_complete_seen, start_time)
|
|
126
|
+
return [nil, offset, task_complete_seen] unless File.exist?(path) && File.size(path) > offset
|
|
127
|
+
|
|
128
|
+
File.open(path, "r") do |f|
|
|
129
|
+
f.seek(offset)
|
|
130
|
+
f.each_line do |line|
|
|
131
|
+
event = parse_event(line)
|
|
132
|
+
next unless event
|
|
133
|
+
|
|
134
|
+
task_complete_seen = true if event_type(event) == "task_complete"
|
|
135
|
+
if matches?(event, predicate, task_complete_seen)
|
|
136
|
+
return [emit_event_match(event, start_time), f.pos, task_complete_seen]
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
offset = f.pos
|
|
135
140
|
end
|
|
141
|
+
|
|
142
|
+
[nil, offset, task_complete_seen]
|
|
136
143
|
end
|
|
137
144
|
|
|
138
145
|
def parse_event(line)
|
|
139
|
-
JSON.parse(line)
|
|
146
|
+
event = JSON.parse(line)
|
|
147
|
+
event.is_a?(Hash) ? event : nil
|
|
140
148
|
rescue JSON::ParserError
|
|
141
|
-
|
|
149
|
+
legacy_type = line.to_s.strip
|
|
150
|
+
return nil unless LEGACY_EVENT_TYPES.include?(legacy_type)
|
|
151
|
+
|
|
152
|
+
{ "type" => legacy_type }
|
|
142
153
|
end
|
|
143
154
|
|
|
144
|
-
def
|
|
155
|
+
def event_type(event)
|
|
145
156
|
type = event["type"]
|
|
157
|
+
return type if type.is_a?(String) && !type.empty?
|
|
158
|
+
|
|
159
|
+
legacy_type = event["terminal_event"] || event["event"]
|
|
160
|
+
legacy_type = legacy_type.to_s
|
|
161
|
+
LEGACY_EVENT_TYPES.include?(legacy_type) ? legacy_type : nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def matches?(event, predicate, task_complete_seen)
|
|
165
|
+
type = event_type(event)
|
|
146
166
|
case predicate
|
|
147
167
|
when "task_complete"
|
|
148
168
|
type == "task_complete"
|
|
@@ -159,7 +179,7 @@ module Harnex
|
|
|
159
179
|
puts JSON.generate(
|
|
160
180
|
ok: true,
|
|
161
181
|
id: @options[:id],
|
|
162
|
-
event: event
|
|
182
|
+
event: event_type(event),
|
|
163
183
|
seq: event["seq"],
|
|
164
184
|
waited_seconds: waited
|
|
165
185
|
)
|
|
@@ -227,6 +247,7 @@ module Harnex
|
|
|
227
247
|
|
|
228
248
|
loop do
|
|
229
249
|
unless Harnex.alive_pid?(target_pid)
|
|
250
|
+
await_exit_status(exit_path)
|
|
230
251
|
return read_exit_status(exit_path, @options[:id])
|
|
231
252
|
end
|
|
232
253
|
|
|
@@ -239,6 +260,26 @@ module Harnex
|
|
|
239
260
|
end
|
|
240
261
|
end
|
|
241
262
|
|
|
263
|
+
# Subprocess death races the parent's DISPATCH-row write; the exit-status
|
|
264
|
+
# file is written *after* the row, so polling it bounds the race.
|
|
265
|
+
def await_exit_status(exit_path)
|
|
266
|
+
return if File.exist?(exit_path)
|
|
267
|
+
|
|
268
|
+
grace_deadline = Time.now + exit_status_grace_seconds
|
|
269
|
+
until File.exist?(exit_path) || Time.now >= grace_deadline
|
|
270
|
+
sleep EXIT_STATUS_GRACE_POLL_INTERVAL
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def exit_status_grace_seconds
|
|
275
|
+
override = ENV["HARNEX_EXIT_STATUS_GRACE_SECONDS"]
|
|
276
|
+
return EXIT_STATUS_GRACE_SECONDS_DEFAULT if override.to_s.strip.empty?
|
|
277
|
+
|
|
278
|
+
Float(override)
|
|
279
|
+
rescue ArgumentError
|
|
280
|
+
EXIT_STATUS_GRACE_SECONDS_DEFAULT
|
|
281
|
+
end
|
|
282
|
+
|
|
242
283
|
def fetch_agent_state(host, port, token)
|
|
243
284
|
uri = URI("http://#{host}:#{port}/status")
|
|
244
285
|
request = Net::HTTP::Get.new(uri)
|
data/lib/harnex/core.rb
CHANGED
|
@@ -85,10 +85,10 @@ module Harnex
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def default_summary_out_path(repo_root)
|
|
88
|
-
|
|
89
|
-
return nil
|
|
88
|
+
root = repo_root.to_s
|
|
89
|
+
return nil if root.empty?
|
|
90
90
|
|
|
91
|
-
File.join(
|
|
91
|
+
File.join(root, ".harnex", "dispatch.jsonl")
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
def git_capture_start(repo_root)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "json"
|
|
3
|
+
require "open3"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Harnex
|
|
7
|
+
module DispatchHistory
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
MAX_REPO_WALK_LEVELS = 10
|
|
11
|
+
|
|
12
|
+
def global_path
|
|
13
|
+
File.join(STATE_DIR, "dispatch.jsonl")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def path_for(start_path = Dir.pwd, global: false)
|
|
17
|
+
return global_path if global
|
|
18
|
+
|
|
19
|
+
repo_root = find_git_root(start_path)
|
|
20
|
+
return global_path unless repo_root
|
|
21
|
+
|
|
22
|
+
File.join(repo_root, ".harnex", "dispatch.jsonl")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def find_git_root(start_path)
|
|
26
|
+
path = File.expand_path(start_path.to_s.empty? ? Dir.pwd : start_path)
|
|
27
|
+
path = File.dirname(path) unless File.directory?(path)
|
|
28
|
+
|
|
29
|
+
git_root = git_toplevel(path)
|
|
30
|
+
return git_root if git_root
|
|
31
|
+
|
|
32
|
+
(MAX_REPO_WALK_LEVELS + 1).times do
|
|
33
|
+
return path if File.directory?(File.join(path, ".git"))
|
|
34
|
+
|
|
35
|
+
parent = File.dirname(path)
|
|
36
|
+
break if parent == path
|
|
37
|
+
|
|
38
|
+
path = parent
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def git_toplevel(path)
|
|
45
|
+
output, status = Open3.capture2("git", "-C", path, "rev-parse", "--show-toplevel", err: File::NULL)
|
|
46
|
+
root = output.to_s.strip
|
|
47
|
+
return root if status.success? && !root.empty?
|
|
48
|
+
|
|
49
|
+
nil
|
|
50
|
+
rescue StandardError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def append(path, record)
|
|
55
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
56
|
+
line = JSON.generate(record) + "\n"
|
|
57
|
+
File.open(path, File::WRONLY | File::APPEND | File::CREAT, 0o644) do |file|
|
|
58
|
+
file.flock(File::LOCK_EX)
|
|
59
|
+
file.write(line)
|
|
60
|
+
ensure
|
|
61
|
+
file.flock(File::LOCK_UN) unless file.closed?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def build_record(session)
|
|
66
|
+
ended_at = session.ended_at || Time.now
|
|
67
|
+
status, terminal_event = classify(session)
|
|
68
|
+
{
|
|
69
|
+
schema_version: 1,
|
|
70
|
+
id: session.id,
|
|
71
|
+
description: session.description,
|
|
72
|
+
cli: session.adapter.key,
|
|
73
|
+
started_at: session.started_at.utc.iso8601,
|
|
74
|
+
ended_at: ended_at.utc.iso8601,
|
|
75
|
+
duration_s: (ended_at - session.started_at).to_i,
|
|
76
|
+
status: status,
|
|
77
|
+
terminal_event: terminal_event,
|
|
78
|
+
commit_sha: commit_sha(session.git_start, session.git_end),
|
|
79
|
+
tier: session.__send__(:meta_hash)["tier"],
|
|
80
|
+
meta: session.__send__(:meta_hash),
|
|
81
|
+
summary_out_path: session.summary_out,
|
|
82
|
+
events_log_path: session.events_log_path,
|
|
83
|
+
tmux_state: tmux_state(session.__send__(:summary_tmux_session))
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def classify(session)
|
|
88
|
+
return ["completed", "task_complete"] if session.task_complete?
|
|
89
|
+
return ["timeout", "timeout"] if session.exit_code == 124
|
|
90
|
+
return ["killed", "process_kill"] if session.term_signal
|
|
91
|
+
return ["completed", "process_exit"] if session.exit_code == 0
|
|
92
|
+
|
|
93
|
+
["failed", "dispatch_failed"]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def commit_sha(git_start, git_end)
|
|
97
|
+
start_sha = git_start[:sha].to_s
|
|
98
|
+
end_sha = git_end[:sha].to_s
|
|
99
|
+
return nil if start_sha.empty? || end_sha.empty? || start_sha == end_sha
|
|
100
|
+
|
|
101
|
+
end_sha
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def tmux_state(tmux_session)
|
|
105
|
+
return "torn-down" if tmux_session.to_s.empty?
|
|
106
|
+
|
|
107
|
+
system("tmux", "has-session", "-t", tmux_session.to_s, out: File::NULL, err: File::NULL) ? "live" : "torn-down"
|
|
108
|
+
rescue StandardError
|
|
109
|
+
"torn-down"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|