turnkit 0.4.0 → 0.4.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +170 -49
  4. data/UPGRADE.md +29 -0
  5. data/UPGRADE_TO_0_4_2.md +425 -0
  6. data/lib/{turnkit/generators → generators}/turnkit/install/templates/initializer.rb +7 -6
  7. data/lib/turnkit/{stores/active_record_store.rb → active_record_store.rb} +18 -4
  8. data/lib/turnkit/adapters/codex.rb +8 -11
  9. data/lib/turnkit/adapters/ruby_llm.rb +68 -44
  10. data/lib/turnkit/agent.rb +43 -10
  11. data/lib/turnkit/budget.rb +1 -1
  12. data/lib/turnkit/client.rb +9 -1
  13. data/lib/turnkit/compaction.rb +46 -46
  14. data/lib/turnkit/cost.rb +29 -31
  15. data/lib/turnkit/image_result.rb +1 -0
  16. data/lib/turnkit/image_tool.rb +2 -2
  17. data/lib/turnkit/media_analysis_result.rb +46 -0
  18. data/lib/turnkit/media_input.rb +208 -0
  19. data/lib/turnkit/message.rb +5 -1
  20. data/lib/turnkit/message_projection.rb +11 -0
  21. data/lib/turnkit/model_request.rb +4 -2
  22. data/lib/turnkit/output_policy.rb +10 -15
  23. data/lib/turnkit/result.rb +12 -0
  24. data/lib/turnkit/run.rb +0 -4
  25. data/lib/turnkit/store.rb +4 -6
  26. data/lib/turnkit/sub_agent_tool.rb +9 -14
  27. data/lib/turnkit/system_prompt.rb +32 -96
  28. data/lib/turnkit/tool.rb +5 -16
  29. data/lib/turnkit/turn.rb +113 -58
  30. data/lib/turnkit/version.rb +1 -1
  31. data/lib/turnkit/view_media_tool.rb +30 -0
  32. data/lib/turnkit.rb +9 -18
  33. metadata +18 -30
  34. data/lib/turnkit/prompt_contribution.rb +0 -13
  35. data/lib/turnkit/rails/railtie.rb +0 -9
  36. data/lib/turnkit/workflow.rb +0 -58
  37. /data/lib/{turnkit/generators → generators}/turnkit/install/templates/conversation.rb +0 -0
  38. /data/lib/{turnkit/generators → generators}/turnkit/install/templates/create_turnkit_tables.rb +0 -0
  39. /data/lib/{turnkit/generators → generators}/turnkit/install/templates/message.rb +0 -0
  40. /data/lib/{turnkit/generators → generators}/turnkit/install/templates/tool_execution.rb +0 -0
  41. /data/lib/{turnkit/generators → generators}/turnkit/install/templates/turn.rb +0 -0
  42. /data/lib/{turnkit/generators → generators}/turnkit/install_generator.rb +0 -0
@@ -0,0 +1,425 @@
1
+ # Upgrade to TurnKit 0.4.2
2
+
3
+ This guide is for upgrading existing TurnKit apps to `0.4.2` from older
4
+ pre-1.0 versions.
5
+
6
+ ## Audit summary
7
+
8
+ The current branch is not a patch-only dependency bump. It removes several
9
+ short-lived public APIs and consolidates the task-runner surface around
10
+ `TurnKit::Agent`. Treat `0.4.2` as a breaking pre-1.0 upgrade unless the removed
11
+ compatibility shims are restored before release.
12
+
13
+ If you only use plain `Conversation` turns with the built-in memory store and no
14
+ custom tools, clients, prompt contributors, workflows, or Rails store overrides,
15
+ the upgrade is usually small. Apps using workflow/orchestrator APIs should follow
16
+ the checklist below.
17
+
18
+ ## 1. Update your Gemfile
19
+
20
+ ```ruby
21
+ gem "turnkit", "0.4.2"
22
+ ```
23
+
24
+ If you use TurnKit's default RubyLLM adapter, add RubyLLM explicitly. TurnKit no
25
+ longer depends on it at runtime.
26
+
27
+ ```ruby
28
+ gem "ruby_llm", "~> 1.16"
29
+ ```
30
+
31
+ Codex adapter users and apps with a custom `TurnKit::Client` do not need
32
+ `ruby_llm` unless they also use `TurnKit::Adapters::RubyLLM`.
33
+
34
+ Then update your bundle:
35
+
36
+ ```sh
37
+ bundle update turnkit
38
+ bundle update ruby_llm # only if you added or already use ruby_llm
39
+ ```
40
+
41
+ ## 2. Replace workflows with orchestrator agents
42
+
43
+ `TurnKit::Workflow` was removed. Use `TurnKit::Agent` with
44
+ `orchestrator: true` instead.
45
+
46
+ Before:
47
+
48
+ ```ruby
49
+ workflow = TurnKit::Workflow.new(
50
+ name: "brief_writer",
51
+ instructions: "Create source-grounded briefs.",
52
+ skills: [source_grounded_brief],
53
+ tools: [WebSearch, ReadWebPage],
54
+ max_spend: 0.25
55
+ )
56
+
57
+ run = workflow.run(
58
+ "Create a brief.",
59
+ input: { topic: topic }
60
+ )
61
+ ```
62
+
63
+ After:
64
+
65
+ ```ruby
66
+ agent = TurnKit::Agent.new(
67
+ name: "brief_writer",
68
+ orchestrator: true,
69
+ instructions: "Create source-grounded briefs.",
70
+ skills: [source_grounded_brief],
71
+ tools: [WebSearch, ReadWebPage],
72
+ max_spend: 0.25
73
+ )
74
+
75
+ run = agent.run(
76
+ "Create a brief.",
77
+ input: { topic: topic }
78
+ )
79
+ ```
80
+
81
+ Notes:
82
+
83
+ - `orchestrator: true` prepends the same autonomous-task preamble that workflows
84
+ used and defaults the agent prompt mode to `:task`.
85
+ - `workflow.agent(**overrides)` no longer exists. Construct another
86
+ `TurnKit::Agent` with the desired options.
87
+ - Per-run workflow overrides such as `workflow.run(..., max_spend: 0.10)` no
88
+ longer have a direct equivalent. Configure those limits on the agent; agents
89
+ are cheap to construct.
90
+
91
+ ## 3. Update `Agent#run` call sites
92
+
93
+ `Agent#run` now requires the task as the first positional argument.
94
+
95
+ ```ruby
96
+ # Before
97
+ agent.run(task: "Classify this lead.", input: lead)
98
+
99
+ # After
100
+ agent.run("Classify this lead.", input: lead)
101
+ ```
102
+
103
+ `input:`, `async:`, `subject:`, `metadata:`, and other run options remain keyword
104
+ arguments.
105
+
106
+ ## 4. Rename global model configuration
107
+
108
+ The temporary `TurnKit.model` aliases were removed.
109
+
110
+ ```ruby
111
+ # Before
112
+ TurnKit.model = "gpt-4.1-mini"
113
+ model = TurnKit.model
114
+
115
+ # After
116
+ TurnKit.default_model = "gpt-4.1-mini"
117
+ model = TurnKit.default_model
118
+ ```
119
+
120
+ In `TurnKit.configure`, use `config.default_model = ...`.
121
+
122
+ ## 5. Rename run accessors
123
+
124
+ Update code that reads completed run results or tool activity:
125
+
126
+ | Before | After |
127
+ | --- | --- |
128
+ | `run.output` | `run.output_text` |
129
+ | `run.tool_calls` | `run.tool_executions` |
130
+ | `run.steps` | `run.turn_records.length` if you need the count |
131
+ | `run.persisted?` | remove; runs are persisted wrappers around turns |
132
+
133
+ `run.output_data`, `run.usage`, `run.cost`, `run.policy_audit`, and
134
+ `run.policy_clean?` remain available.
135
+
136
+ ## 6. Update custom tools
137
+
138
+ TurnKit now passes framework context only as `context:`. The older
139
+ `turnkit_context:` keyword was removed.
140
+
141
+ ```ruby
142
+ # Before
143
+ def call(title:, body:, turnkit_context:)
144
+ turn = turnkit_context.turn
145
+ { id: SaveReport.call(title:, body:, turn:) }
146
+ end
147
+
148
+ # After
149
+ def call(title:, body:, context:)
150
+ turn = context.turn
151
+ { id: SaveReport.call(title:, body:, turn:) }
152
+ end
153
+ ```
154
+
155
+ `context` is now a reserved tool parameter name. Do not expose a model-visible
156
+ tool parameter named `context`:
157
+
158
+ ```ruby
159
+ # Do not do this in 0.4.2
160
+ parameter :context, :string
161
+ ```
162
+
163
+ If a tool class needs constructor arguments, register an instance instead of the
164
+ class:
165
+
166
+ ```ruby
167
+ agent = TurnKit::Agent.new(
168
+ name: "researcher",
169
+ tools: [WebSearch.new(client: SearchClient.new)]
170
+ )
171
+ ```
172
+
173
+ ## 7. Update sub-agent calls
174
+
175
+ `TurnKit::SubAgentTool` exposes a single model-visible `task` parameter. If your
176
+ sub-agent usage previously separated `task` and `context`, include the necessary
177
+ context in the task text itself.
178
+
179
+ Before:
180
+
181
+ ```json
182
+ { "task": "Draft headlines", "context": "Product: PhotoDay" }
183
+ ```
184
+
185
+ After:
186
+
187
+ ```json
188
+ { "task": "Draft headlines for Product: PhotoDay." }
189
+ ```
190
+
191
+ ## 8. Replace prompt contributor APIs
192
+
193
+ These prompt extension APIs were removed:
194
+
195
+ - `TurnKit.system_prompt_contributors`
196
+ - `TurnKit.model_prompt_contributors`
197
+ - `TurnKit::PromptContribution`
198
+ - prompt section override objects
199
+ - `TurnKit::SystemPrompt::CACHE_BOUNDARY`
200
+ - `TurnKit::SystemPrompt.split_cache_boundary`
201
+
202
+ Use per-agent `system_prompt:` instead. A string replaces the generated prompt.
203
+ A callable receives a `TurnKit::SystemPrompt` object.
204
+
205
+ ```ruby
206
+ agent = TurnKit::Agent.new(
207
+ name: "reporter",
208
+ system_prompt: ->(prompt) {
209
+ [
210
+ prompt.stable,
211
+ prompt.section(:tools),
212
+ prompt.dynamic
213
+ ].reject(&:empty?).join("\n\n")
214
+ }
215
+ )
216
+ ```
217
+
218
+ Available generated-prompt APIs:
219
+
220
+ - `prompt.to_s`
221
+ - `prompt.stable`
222
+ - `prompt.dynamic`
223
+ - `prompt.section(:tools)` and other section names
224
+
225
+ `TurnKit.prompt_sections`, per-agent `prompt_sections:`,
226
+ `TurnKit.prompt_behavior`, and `TurnKit.context_contributors` remain available.
227
+
228
+ ## 9. Update custom clients and adapters
229
+
230
+ TurnKit no longer sniffs custom client method signatures before calling them.
231
+ Custom clients should subclass `TurnKit::Client` or accept the full keyword
232
+ contracts.
233
+
234
+ At minimum, update `chat` to accept `dynamic_instructions:`:
235
+
236
+ ```ruby
237
+ def chat(model:, messages:, tools:, instructions:, dynamic_instructions: nil,
238
+ temperature: nil, thinking: nil, output_schema: nil, metadata: nil,
239
+ on_event: nil)
240
+ full_prompt = [instructions, dynamic_instructions].compact.join("\n\n")
241
+ # call provider...
242
+ end
243
+ ```
244
+
245
+ If your client supports image generation or media analysis, also accept the full
246
+ `paint` and `view_media` keyword sets from `TurnKit::Client`, including
247
+ `metadata:` and `on_event:`.
248
+
249
+ Prompt caching changed from an embedded cache-boundary marker to explicit stable
250
+ and dynamic instructions. Adapters that support provider prompt caching should
251
+ cache `instructions` and append `dynamic_instructions` per turn.
252
+
253
+ ## 10. Update custom stores
254
+
255
+ Built-in `TurnKit::MemoryStore` and `TurnKit::ActiveRecordStore` already support
256
+ the new claiming behavior.
257
+
258
+ If you maintain a custom store, implement `claim_turn` as an atomic
259
+ compare-and-set from `from` to `to`. It must return the claimed turn record when
260
+ the claim succeeds and `nil` when another worker already claimed or completed
261
+ the turn.
262
+
263
+ ```ruby
264
+ def claim_turn(id, from: "pending", to: "running", **attributes)
265
+ # Atomically update only when uid/id and status both match.
266
+ # Return nil if no row was updated.
267
+ end
268
+ ```
269
+
270
+ The old default implementation was intentionally removed because it was not safe
271
+ for concurrent workers.
272
+
273
+ ## 11. Update Rails store configuration
274
+
275
+ The generated table and model names remain compatible, but custom Active Record
276
+ class configuration moved from global TurnKit attributes to the store
277
+ constructor.
278
+
279
+ Before:
280
+
281
+ ```ruby
282
+ TurnKit.store = TurnKit::ActiveRecordStore.new
283
+ TurnKit.conversation_record_class = "My::Conversation"
284
+ TurnKit.turn_record_class = "My::Turn"
285
+ TurnKit.message_record_class = "My::Message"
286
+ TurnKit.tool_execution_record_class = "My::ToolExecution"
287
+ ```
288
+
289
+ After:
290
+
291
+ ```ruby
292
+ TurnKit.store = TurnKit::ActiveRecordStore.new(
293
+ conversation_class: "My::Conversation",
294
+ turn_class: "My::Turn",
295
+ message_class: "My::Message",
296
+ tool_execution_class: "My::ToolExecution"
297
+ )
298
+ ```
299
+
300
+ Apps that used `require "turnkit"` can keep doing that. If you directly required
301
+ the old internal Active Record store path, update it:
302
+
303
+ ```ruby
304
+ # Before
305
+ require "turnkit/stores/active_record_store"
306
+
307
+ # After
308
+ require "turnkit/active_record_store"
309
+ ```
310
+
311
+ If you manually required `turnkit/rails/railtie`, remove that require. The Rails
312
+ install generator now lives at the conventional `lib/generators` path and should
313
+ still be available as:
314
+
315
+ ```sh
316
+ bin/rails generate turnkit:install
317
+ ```
318
+
319
+ ## 12. Check database migrations for older Rails installs
320
+
321
+ New installs already include all current columns.
322
+
323
+ Existing Rails installs from before structured/image/media output persistence
324
+ should add `output_data` to turns if they do not already have it:
325
+
326
+ ```ruby
327
+ class AddOutputDataToTurnkitTurns < ActiveRecord::Migration[7.1]
328
+ def change
329
+ add_column :turnkit_turns, :output_data, :json
330
+ end
331
+ end
332
+ ```
333
+
334
+ If you customized the table prefix when installing TurnKit, use your actual turns
335
+ table name instead of `turnkit_turns`.
336
+
337
+ Upgrades from the `0.2.x` series also need the `0.3.0` message-schema migration:
338
+ message content is stored as ordered typed parts in `turnkit_messages.content`,
339
+ with text derived from content. For greenfield or disposable development data,
340
+ the simplest route is usually to regenerate the current install migration and
341
+ recreate the TurnKit tables.
342
+
343
+ ## 13. Update custom cost rates
344
+
345
+ `TurnKit.cost_rates` now accepts only these component keys, expressed as USD per
346
+ million tokens:
347
+
348
+ - `input`
349
+ - `output`
350
+ - `cache_read`
351
+ - `cache_write`
352
+ - `thinking`
353
+
354
+ Before:
355
+
356
+ ```ruby
357
+ TurnKit.cost_rates["gpt-5"] = {
358
+ input: 1.25,
359
+ output: 10.0,
360
+ cached_input: 0.125,
361
+ reasoning: 10.0
362
+ }
363
+ ```
364
+
365
+ After:
366
+
367
+ ```ruby
368
+ TurnKit.cost_rates["gpt-5"] = {
369
+ input: 1.25,
370
+ output: 10.0,
371
+ cache_read: 0.125,
372
+ thinking: 10.0
373
+ }
374
+ ```
375
+
376
+ Old aliases such as `cached_input`, `cache_creation`, `reasoning`, and
377
+ `*_per_million` now raise `TurnKit::ConfigError`.
378
+
379
+ ## 14. Update media analysis checks
380
+
381
+ `MediaAnalysisResult#structured?` was removed. Use `data?` instead.
382
+
383
+ ```ruby
384
+ # Before
385
+ analysis.structured?
386
+
387
+ # After
388
+ analysis.data?
389
+ ```
390
+
391
+ ## 15. Non-breaking changes to know about
392
+
393
+ - Turn runtime state such as iteration counts and output-policy audits now lives
394
+ under `options["state"]`. Reads fall back to the older top-level keys, so no
395
+ data migration is required for this change.
396
+ - Compaction examples now use symbol keys, but string keys are still accepted.
397
+ - Built-in memory and Active Record stores already implement atomic turn claims.
398
+ - Generated Rails model/table names still work. Only custom class configuration
399
+ moved to `ActiveRecordStore.new(...)`.
400
+ - Removing the transitive `ruby_llm` dependency does not affect Codex adapter or
401
+ custom-client users.
402
+
403
+ ## 16. Suggested upgrade order
404
+
405
+ 1. Create a branch and update the Gemfile.
406
+ 2. Add `ruby_llm`, if you use the default RubyLLM adapter.
407
+ 3. Update workflows to `Agent.new(orchestrator: true)`.
408
+ 4. Rename `Agent#run`, run accessors, and `TurnKit.default_model` call sites.
409
+ 5. Update custom tools, sub-agents, clients, stores, cost rates, and Rails store
410
+ configuration.
411
+ 6. Add the `output_data` migration if your Rails tables do not have it.
412
+ 7. Run the test suite and at least one real model smoke test for each configured
413
+ adapter.
414
+
415
+ Useful searches:
416
+
417
+ ```sh
418
+ rg "TurnKit::Workflow|TurnKit\.model|run\.output|run\.tool_calls|run\.steps|turnkit_context|structured\?|cached_input|cache_creation|reasoning|system_prompt_contributors|model_prompt_contributors|PromptContribution|conversation_record_class|turn_record_class|message_record_class|tool_execution_record_class"
419
+ ```
420
+
421
+ After the code compiles, run:
422
+
423
+ ```sh
424
+ bundle exec rake test
425
+ ```
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Namespaces: TurnKit:: is the gem's domain layer (agents, conversations,
4
+ # turns). The generated ActiveRecord models in app/models/turnkit live under
5
+ # Turnkit:: and are pure persistence records used by the store below.
6
+ #
7
+ # Pass custom model class names if you moved or renamed the generated models:
8
+ # TurnKit::ActiveRecordStore.new(conversation_class: "My::Conversation", ...)
3
9
  TurnKit.store = TurnKit::ActiveRecordStore.new
4
10
 
5
- TurnKit.conversation_record_class = "Turnkit::Conversation"
6
- TurnKit.turn_record_class = "Turnkit::Turn"
7
- TurnKit.message_record_class = "Turnkit::Message"
8
- TurnKit.tool_execution_record_class = "Turnkit::ToolExecution"
9
-
10
11
  # TurnKit.default_model = "claude-sonnet-4-5"
11
12
  # TurnKit.max_iterations = 25
12
13
  # TurnKit.timeout = 300
@@ -20,6 +21,6 @@ TurnKit.tool_execution_record_class = "Turnkit::ToolExecution"
20
21
  # TurnKit.available_skills = TurnKit::Skill.from_directory(Rails.root.join("app/ai/skills"))
21
22
 
22
23
  # Suggested Rails convention:
23
- # - app/ai/agents/* builds TurnKit::Agent objects for your workflows.
24
+ # - app/ai/agents/* builds TurnKit::Agent objects and orchestrator agents.
24
25
  # - app/ai/tools/* defines TurnKit::Tool subclasses.
25
26
  # - app/ai/skills/* stores reusable Markdown skill files.
@@ -1,7 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurnKit
4
+ # Persists TurnKit records through ActiveRecord model classes generated by
5
+ # `rails g turnkit:install`.
6
+ #
7
+ # Note the namespaces: TurnKit:: is this gem's domain layer; the generated
8
+ # Rails models live under Turnkit:: (Rails-inflection friendly) and are pure
9
+ # persistence records. Turnkit::Conversation (a database row) is not
10
+ # TurnKit::Conversation (the domain object).
4
11
  class ActiveRecordStore < Store
12
+ def initialize(conversation_class: "Turnkit::Conversation", turn_class: "Turnkit::Turn", message_class: "Turnkit::Message", tool_execution_class: "Turnkit::ToolExecution")
13
+ @conversation_class_name = conversation_class
14
+ @turn_class_name = turn_class
15
+ @message_class_name = message_class
16
+ @tool_execution_class_name = tool_execution_class
17
+ end
18
+
5
19
  def create_conversation(attributes)
6
20
  record = conversation_class.create!(record_attributes(attributes, id_key: "uid"))
7
21
  conversation_hash(record)
@@ -145,10 +159,10 @@ module TurnKit
145
159
  end
146
160
 
147
161
  private
148
- def conversation_class = constantize(TurnKit.conversation_record_class || "Turnkit::Conversation")
149
- def turn_class = constantize(TurnKit.turn_record_class || "Turnkit::Turn")
150
- def message_class = constantize(TurnKit.message_record_class || "Turnkit::Message")
151
- def tool_execution_class = constantize(TurnKit.tool_execution_record_class || "Turnkit::ToolExecution")
162
+ def conversation_class = constantize(@conversation_class_name)
163
+ def turn_class = constantize(@turn_class_name)
164
+ def message_class = constantize(@message_class_name)
165
+ def tool_execution_class = constantize(@tool_execution_class_name)
152
166
 
153
167
  def constantize(name)
154
168
  name.to_s.split("::").inject(Object) { |mod, part| mod.const_get(part) }
@@ -22,9 +22,12 @@ module TurnKit
22
22
 
23
23
  def validate!(model:)
24
24
  raise ModelAccessError, "codex command is required" if command.empty?
25
- raise ModelAccessError, "#{command.inspect} was not found. Install OpenAI Codex CLI and run `codex login --device-auth`." unless executable?(command)
26
25
 
27
- stdout, stderr, status = @runner.call([ command, "login", "status" ], stdin_data: nil, chdir: working_directory)
26
+ begin
27
+ stdout, stderr, status = @runner.call([ command, "login", "status" ], stdin_data: nil, chdir: working_directory)
28
+ rescue Errno::ENOENT
29
+ raise ModelAccessError, "#{command.inspect} was not found. Install OpenAI Codex CLI and run `codex login --device-auth`."
30
+ end
28
31
  return true if status.success?
29
32
 
30
33
  message = [ stderr, stdout ].join("\n").strip
@@ -32,12 +35,13 @@ module TurnKit
32
35
  raise ModelAccessError, [ "Codex is not authenticated.", message, hint ].reject(&:empty?).join(" ")
33
36
  end
34
37
 
35
- def chat(model:, messages:, tools:, instructions:, temperature: nil, thinking: nil, output_schema: nil, metadata: nil, on_event: nil)
38
+ def chat(model:, messages:, tools:, instructions:, dynamic_instructions: nil, temperature: nil, thinking: nil, output_schema: nil, metadata: nil, on_event: nil)
36
39
  raise ToolError, "TurnKit tools are not supported by the Codex adapter; Codex uses its own local tools" if Array(tools).any?
37
40
 
41
+ full_instructions = [ instructions.to_s, dynamic_instructions.to_s ].reject(&:empty?).join("\n\n")
38
42
  with_tempfiles(output_schema: output_schema) do |schema_file, output_file|
39
43
  command = exec_command(model: model, schema_file: schema_file&.path, output_file: output_file.path)
40
- stdout, stderr, status = @runner.call(command, stdin_data: prompt_for(messages: messages, instructions: instructions), chdir: working_directory)
44
+ stdout, stderr, status = @runner.call(command, stdin_data: prompt_for(messages: messages, instructions: full_instructions), chdir: working_directory)
41
45
  emit_codex_events(stdout, on_event: on_event)
42
46
  raise ModelAccessError, stderr.strip.empty? ? "codex exec failed" : stderr.strip unless status.success?
43
47
 
@@ -144,13 +148,6 @@ module TurnKit
144
148
  end
145
149
  end
146
150
 
147
- def executable?(name)
148
- return true if @runner != method(:run_command)
149
- return File.executable?(name) if name.include?(File::SEPARATOR)
150
-
151
- ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? { |path| File.executable?(File.join(path, name)) }
152
- end
153
-
154
151
  def run_command(command, stdin_data:, chdir:)
155
152
  stdout, stderr, status = Open3.capture3(*command, stdin_data: stdin_data, chdir: chdir)
156
153
  [ stdout, stderr, status ]