miniswen 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +3 -1
- data/lib/miniswen/agent.rb +12 -6
- data/lib/miniswen/cli/reporter.rb +104 -0
- data/lib/miniswen/cli.rb +36 -82
- data/lib/miniswen/environment/docker.rb +69 -0
- data/lib/miniswen/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa5e319d6fe043262626e21f41e6f74956373aef813c83c6e68b098674e89c42
|
|
4
|
+
data.tar.gz: 3019152e7898917a2284195c156c126316f46e37798e80fb636e579a0f7d56df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f97f7f90851abf72f8886b97f0c9b5274acb8b6a2984986b99def626bd452eff6b9a38f4607b55e71412a0bb577fb76b2968577d7b1447cd026167f4605d4492
|
|
7
|
+
data.tar.gz: 1751cdcae04283c07233a5ae5219f94d0171596d7398731624f7d7f1665330a6fa9b24bf330728a65400ec766fcee89c01a85cd6c7fe75ed10caba70a321e2de
|
data/README.md
CHANGED
|
@@ -76,6 +76,7 @@ environment:
|
|
|
76
76
|
agent:
|
|
77
77
|
name: miniswen-installed
|
|
78
78
|
model: openrouter/z-ai/glm-5.2 # any model RubyLLM can reach; a list benchmarks several in one run
|
|
79
|
+
# append #<effort> (e.g. openrouter/openai/gpt-5.6-luna#xhigh) to pin the reasoning effort
|
|
79
80
|
timeout: 30m
|
|
80
81
|
step_limit: 100
|
|
81
82
|
cost_limit: 5.0
|
|
@@ -167,7 +168,8 @@ An `environment.patch` next to `instruction.md` is always applied, declared or n
|
|
|
167
168
|
export DAYTONA_API_KEY=... # or DAYTONA_TOKEN (if using Daytona)
|
|
168
169
|
export OPENROUTER_API_KEY=... # or ANTHROPIC_API_KEY, OPENAI_API_KEY, ... — matching your model
|
|
169
170
|
|
|
170
|
-
export LEMANS_PROVIDER_ORDER="
|
|
171
|
+
export LEMANS_PROVIDER_ORDER="chutes" # [optional] Pin an OpenRouter model to provider slugs (alias: OPENROUTER_PROVIDER_ORDER)
|
|
172
|
+
export LEMANS_PROVIDER_ORDER="z-ai,chutes" # ...or try them in order
|
|
171
173
|
```
|
|
172
174
|
|
|
173
175
|
### 4. Prove the bench before benchmarking anything
|
data/lib/miniswen/agent.rb
CHANGED
|
@@ -232,11 +232,14 @@ module Miniswen
|
|
|
232
232
|
private attr_reader :max_steps, :max_time, :max_cost, :exec_timeout,
|
|
233
233
|
:clock, :reporter
|
|
234
234
|
|
|
235
|
-
# `model` is a litellm-style name ("openrouter/z-ai/glm-5.2")
|
|
235
|
+
# `model` is a litellm-style name ("openrouter/z-ai/glm-5.2"), optionally
|
|
236
|
+
# suffixed with a reasoning effort ("openrouter/openai/gpt-5.6-luna#xhigh").
|
|
237
|
+
# Limits of 0 or nil are disabled.
|
|
236
238
|
def initialize(model:, environment:, max_steps: 0, max_time: 0, max_cost: nil,
|
|
237
239
|
exec_timeout: 30, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) },
|
|
238
240
|
reporter: nil)
|
|
239
|
-
|
|
241
|
+
name, @effort = model.split("#", 2)
|
|
242
|
+
@provider, @id = name.split("/", 2)
|
|
240
243
|
unless @id
|
|
241
244
|
@id = @provider
|
|
242
245
|
@provider = nil
|
|
@@ -315,8 +318,8 @@ module Miniswen
|
|
|
315
318
|
_, provider = resolved
|
|
316
319
|
env = provider.configuration_requirements.to_h { [ it.to_s.upcase, RubyLLM.config.public_send(it) ] }.compact
|
|
317
320
|
|
|
318
|
-
order =
|
|
319
|
-
env["
|
|
321
|
+
order = provider_order
|
|
322
|
+
env["OPENROUTER_PROVIDER_ORDER"] = order if order
|
|
320
323
|
env
|
|
321
324
|
end
|
|
322
325
|
|
|
@@ -500,7 +503,8 @@ module Miniswen
|
|
|
500
503
|
tools: { bash: @bash_tool },
|
|
501
504
|
temperature: nil,
|
|
502
505
|
model: model_info,
|
|
503
|
-
params: routing_params
|
|
506
|
+
params: routing_params,
|
|
507
|
+
thinking: (RubyLLM::Thinking::Config.new(effort: @effort) if @effort)
|
|
504
508
|
)
|
|
505
509
|
payload(response)
|
|
506
510
|
rescue RubyLLM::Error => e
|
|
@@ -531,12 +535,14 @@ module Miniswen
|
|
|
531
535
|
end
|
|
532
536
|
|
|
533
537
|
def routing_params
|
|
534
|
-
order =
|
|
538
|
+
order = provider_order
|
|
535
539
|
return {} unless order && @provider == "openrouter"
|
|
536
540
|
|
|
537
541
|
{ provider: { order: order.split(",").map(&:strip), allow_fallbacks: false } }
|
|
538
542
|
end
|
|
539
543
|
|
|
544
|
+
def provider_order = ENV["LEMANS_PROVIDER_ORDER"] || ENV["OPENROUTER_PROVIDER_ORDER"]
|
|
545
|
+
|
|
540
546
|
def cost_source
|
|
541
547
|
if local?
|
|
542
548
|
return CostSource.new(name: :local_provider, model: @model,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Miniswen
|
|
4
|
+
class CLI
|
|
5
|
+
# Prints messages and tool calls in real-time. The renderer deliberately
|
|
6
|
+
# keeps the captured (non-TTY) version plain, which makes it useful in CI
|
|
7
|
+
# and when piping a run to a log file too.
|
|
8
|
+
class Reporter
|
|
9
|
+
private attr_reader :io
|
|
10
|
+
|
|
11
|
+
# Tool output can be extremely noisy (for example, a recursive grep or
|
|
12
|
+
# a test runner dumping a log). Keep the normal report useful while
|
|
13
|
+
# allowing -vv to retain the complete output for debugging.
|
|
14
|
+
MAX_TOOL_OUTPUT_CHARS = 1_000
|
|
15
|
+
|
|
16
|
+
def initialize(io = $stdout, verbose: false, tool_output: verbose, reasoning: true)
|
|
17
|
+
@io = io
|
|
18
|
+
@verbose = verbose
|
|
19
|
+
@tool_output = tool_output
|
|
20
|
+
@reasoning = reasoning
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def on_message(message)
|
|
24
|
+
case message[:role].to_s
|
|
25
|
+
when "assistant"
|
|
26
|
+
write_assistant(message)
|
|
27
|
+
when "tool"
|
|
28
|
+
write_tool(message)
|
|
29
|
+
when "user"
|
|
30
|
+
write_block("!", message[:content], :warning)
|
|
31
|
+
else
|
|
32
|
+
write_block("·", message[:content], :muted)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Tool calls are reported separately so the command is visible before
|
|
37
|
+
# its output arrives. It is not added to the trajectory sent to the LLM.
|
|
38
|
+
def on_tool_call(call)
|
|
39
|
+
command = call.dig(:arguments, "command") || call.dig(:arguments, :command)
|
|
40
|
+
return if command.to_s.empty?
|
|
41
|
+
|
|
42
|
+
line = style("$ #{command}", :command)
|
|
43
|
+
io.puts(" #{line}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def print_summary(result)
|
|
47
|
+
write_assistant(result.messages.last)
|
|
48
|
+
write_block("↳", "steps=#{result.steps} · cost=$#{result.cost_usd}", :muted)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def print_failure(result)
|
|
52
|
+
write_block("!", "Miniswen failed: #{result.status}", :warning)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def write_tool(message)
|
|
58
|
+
return write_block("↳", message[:content], :tool) if @tool_output
|
|
59
|
+
|
|
60
|
+
exit_code = message.dig(:observation, :exit_code)
|
|
61
|
+
return if exit_code.nil?
|
|
62
|
+
|
|
63
|
+
if exit_code.zero?
|
|
64
|
+
write_block("↳", "ok", :muted)
|
|
65
|
+
else
|
|
66
|
+
output = truncate(message.dig(:observation, :output).to_s.strip)
|
|
67
|
+
write_block("!", "not ok: exit #{exit_code}\n#{output}", :warning)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def write_assistant(message)
|
|
72
|
+
content = message[:content].to_s.strip
|
|
73
|
+
write_block("∴", message[:thinking], :thinking) if @reasoning && (@verbose || content.empty?)
|
|
74
|
+
write_block("●", content, :assistant)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def write_block(marker, content, tone)
|
|
78
|
+
text = content.to_s.strip
|
|
79
|
+
return if text.empty?
|
|
80
|
+
|
|
81
|
+
text = truncate(text) if %i[tool thinking].include?(tone)
|
|
82
|
+
lines = text.lines(chomp: true)
|
|
83
|
+
io.puts("#{style(marker, tone)} #{style(lines.shift, tone)}")
|
|
84
|
+
lines.each { |line| io.puts(" #{style(line, tone)}") }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def truncate(text)
|
|
88
|
+
return text if @verbose || text.length <= MAX_TOOL_OUTPUT_CHARS
|
|
89
|
+
|
|
90
|
+
head = MAX_TOOL_OUTPUT_CHARS / 2
|
|
91
|
+
tail = MAX_TOOL_OUTPUT_CHARS - head
|
|
92
|
+
omitted = text.length - head - tail
|
|
93
|
+
"#{text[0, head]}\n... [#{omitted} characters omitted] ...\n#{text[-tail, tail]}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def style(text, tone)
|
|
97
|
+
return text unless io.respond_to?(:tty?) && io.tty?
|
|
98
|
+
|
|
99
|
+
colors = { assistant: 36, tool: 32, warning: 33, command: 35, muted: 90, thinking: 90 }
|
|
100
|
+
"\e[#{colors.fetch(tone)}m#{text}\e[0m"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/miniswen/cli.rb
CHANGED
|
@@ -1,88 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
|
+
require "fileutils"
|
|
4
5
|
|
|
5
6
|
require "miniswen/version"
|
|
7
|
+
require "miniswen/cli/reporter"
|
|
6
8
|
|
|
7
9
|
module Miniswen
|
|
8
10
|
class CLI # :nodoc:
|
|
9
|
-
# Prints messages and tool calls in real-time. The renderer deliberately
|
|
10
|
-
# keeps the captured (non-TTY) version plain, which makes it useful in CI
|
|
11
|
-
# and when piping a run to a log file too.
|
|
12
|
-
class Reporter
|
|
13
|
-
private attr_reader :io
|
|
14
|
-
|
|
15
|
-
# Tool output can be extremely noisy (for example, a recursive grep or
|
|
16
|
-
# a test runner dumping a log). Keep the normal report useful while
|
|
17
|
-
# allowing -vv to retain the complete output for debugging.
|
|
18
|
-
MAX_TOOL_OUTPUT_CHARS = 1_000
|
|
19
|
-
|
|
20
|
-
def initialize(io = $stdout, verbose: false)
|
|
21
|
-
@io = io
|
|
22
|
-
@verbose = verbose
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def on_message(message)
|
|
26
|
-
case message[:role].to_s
|
|
27
|
-
when "assistant"
|
|
28
|
-
write_block("●", message[:content], :assistant)
|
|
29
|
-
when "tool"
|
|
30
|
-
write_block("↳", message[:content], :tool)
|
|
31
|
-
when "user"
|
|
32
|
-
write_block("!", message[:content], :warning)
|
|
33
|
-
else
|
|
34
|
-
write_block("·", message[:content], :muted)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Tool calls are reported separately so the command is visible before
|
|
39
|
-
# its output arrives. It is not added to the trajectory sent to the LLM.
|
|
40
|
-
def on_tool_call(call)
|
|
41
|
-
command = call.dig(:arguments, "command") || call.dig(:arguments, :command)
|
|
42
|
-
return if command.to_s.empty?
|
|
43
|
-
|
|
44
|
-
line = style("$ #{command}", :command)
|
|
45
|
-
io.puts(" #{line}")
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def print_summary(result)
|
|
49
|
-
write_block("●", result.messages.last[:content], :assistant)
|
|
50
|
-
write_block("↳", "steps=#{result.steps} · cost=$#{result.cost_usd}", :muted)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def print_failure(result)
|
|
54
|
-
write_block("!", "Miniswen failed: #{result.status}", :warning)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
def write_block(marker, content, tone)
|
|
60
|
-
text = content.to_s.strip
|
|
61
|
-
return if text.empty?
|
|
62
|
-
|
|
63
|
-
text = truncate_tool_output(text) if tone == :tool
|
|
64
|
-
lines = text.lines(chomp: true)
|
|
65
|
-
io.puts("#{style(marker, tone)} #{style(lines.shift, tone)}")
|
|
66
|
-
lines.each { |line| io.puts(" #{style(line, tone)}") }
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def truncate_tool_output(text)
|
|
70
|
-
return text if @verbose || text.length <= MAX_TOOL_OUTPUT_CHARS
|
|
71
|
-
|
|
72
|
-
head = MAX_TOOL_OUTPUT_CHARS / 2
|
|
73
|
-
tail = MAX_TOOL_OUTPUT_CHARS - head
|
|
74
|
-
omitted = text.length - head - tail
|
|
75
|
-
"#{text[0, head]}\n... [#{omitted} characters omitted] ...\n#{text[-tail, tail]}"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def style(text, tone)
|
|
79
|
-
return text unless io.respond_to?(:tty?) && io.tty?
|
|
80
|
-
|
|
81
|
-
colors = { assistant: 36, tool: 32, warning: 33, command: 35, muted: 90 }
|
|
82
|
-
"\e[#{colors.fetch(tone)}m#{text}\e[0m"
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
11
|
attr_reader :instruction, :model, :options
|
|
87
12
|
|
|
88
13
|
def initialize
|
|
@@ -90,6 +15,8 @@ module Miniswen
|
|
|
90
15
|
@model = ENV.fetch("MINISWEN_MODEL", nil)
|
|
91
16
|
@options = {}
|
|
92
17
|
@verbose = false
|
|
18
|
+
@show_output = false
|
|
19
|
+
@reasoning = true
|
|
93
20
|
@quiet = false
|
|
94
21
|
@results_path = nil
|
|
95
22
|
@atif_path = nil
|
|
@@ -110,8 +37,16 @@ module Miniswen
|
|
|
110
37
|
|
|
111
38
|
require "miniswen/local"
|
|
112
39
|
|
|
113
|
-
reporter = @quiet ? nil : Reporter.new(verbose: @verbose)
|
|
114
|
-
|
|
40
|
+
reporter = @quiet ? nil : Reporter.new(verbose: @verbose, tool_output: @verbose || @show_output, reasoning: @reasoning)
|
|
41
|
+
environment =
|
|
42
|
+
if @docker_id
|
|
43
|
+
require "miniswen/environment/docker"
|
|
44
|
+
Environment::Docker.new(@docker_id)
|
|
45
|
+
else
|
|
46
|
+
Local.new
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
agent = Agent.new(model:, reporter:, environment:, **options)
|
|
115
50
|
|
|
116
51
|
begin
|
|
117
52
|
result = agent.run(instruction)
|
|
@@ -133,8 +68,15 @@ module Miniswen
|
|
|
133
68
|
private
|
|
134
69
|
|
|
135
70
|
def write_results(result)
|
|
136
|
-
|
|
137
|
-
|
|
71
|
+
if @results_path
|
|
72
|
+
FileUtils.mkdir_p(File.dirname(@results_path))
|
|
73
|
+
File.write(@results_path, JSON.generate(result.to_h))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if @atif_path
|
|
77
|
+
FileUtils.mkdir_p(File.dirname(@atif_path))
|
|
78
|
+
write_atif(result)
|
|
79
|
+
end
|
|
138
80
|
end
|
|
139
81
|
|
|
140
82
|
def error_message(error)
|
|
@@ -162,7 +104,7 @@ module Miniswen
|
|
|
162
104
|
end
|
|
163
105
|
|
|
164
106
|
opts.on("-p INSTRUCTION", "--prompt=INSTRUCTION", String, "Instruction prompt") do |v|
|
|
165
|
-
@instruction = v
|
|
107
|
+
@instruction = File.file?(v) ? File.read(v) : v
|
|
166
108
|
end
|
|
167
109
|
|
|
168
110
|
opts.on("--max-steps=STEPS", Integer, "Max steps count") do |v|
|
|
@@ -185,6 +127,14 @@ module Miniswen
|
|
|
185
127
|
@quiet = true
|
|
186
128
|
end
|
|
187
129
|
|
|
130
|
+
opts.on("--show-output", "Print tool output instead of just the exit status") do
|
|
131
|
+
@show_output = true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
opts.on("--no-reasoning", "Hide the model's reasoning") do
|
|
135
|
+
@reasoning = false
|
|
136
|
+
end
|
|
137
|
+
|
|
188
138
|
opts.on("--results-path=PATH", String, "Write the run result as JSON to PATH") do |v|
|
|
189
139
|
@results_path = v
|
|
190
140
|
end
|
|
@@ -193,6 +143,10 @@ module Miniswen
|
|
|
193
143
|
@atif_path = v
|
|
194
144
|
end
|
|
195
145
|
|
|
146
|
+
opts.on("--docker=ID", String, "Docker container ID to exec commands on") do |v|
|
|
147
|
+
@docker_id = v
|
|
148
|
+
end
|
|
149
|
+
|
|
196
150
|
opts.on("--refresh-registry", "Refresh the model registry, persist it, and exit") do
|
|
197
151
|
@refresh_registry = true
|
|
198
152
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
require "miniswen/environment"
|
|
6
|
+
|
|
7
|
+
module Miniswen
|
|
8
|
+
class Environment
|
|
9
|
+
class Docker < self
|
|
10
|
+
TIMEOUT_MARKED_EXIT_CODES = [ 124, 143 ].freeze
|
|
11
|
+
|
|
12
|
+
private attr_reader :id
|
|
13
|
+
|
|
14
|
+
def initialize(id)
|
|
15
|
+
@id = id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def exec(command, timeout: nil, env: nil)
|
|
19
|
+
argv = [ "docker", "exec" ]
|
|
20
|
+
env&.each { |key, value| argv += [ "--env", "#{key}=#{value}" ] }
|
|
21
|
+
argv << id
|
|
22
|
+
argv += [ "timeout", timeout.ceil.to_s ] if timeout&.positive?
|
|
23
|
+
argv += [ "sh", "-c", command ]
|
|
24
|
+
|
|
25
|
+
Open3.popen2e(*argv) do |stdin, pipe, wait|
|
|
26
|
+
stdin.close
|
|
27
|
+
deadline = (now + timeout + 10 if timeout&.positive?) # add some slack
|
|
28
|
+
output = +""
|
|
29
|
+
timed_out = false
|
|
30
|
+
|
|
31
|
+
loop do
|
|
32
|
+
remaining = deadline && deadline - now
|
|
33
|
+
if remaining && remaining <= 0
|
|
34
|
+
timed_out = true
|
|
35
|
+
kill(wait.pid)
|
|
36
|
+
break
|
|
37
|
+
end
|
|
38
|
+
next unless pipe.wait_readable(remaining)
|
|
39
|
+
|
|
40
|
+
chunk = pipe.read_nonblock(65_536, exception: false)
|
|
41
|
+
break if chunk.nil?
|
|
42
|
+
next if chunk == :wait_readable
|
|
43
|
+
|
|
44
|
+
output << chunk
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
status = wait.value
|
|
48
|
+
exit_code = timed_out ? 124 : (status.exitstatus || 1)
|
|
49
|
+
output = output.force_encoding(Encoding::UTF_8).scrub
|
|
50
|
+
if timeout&.positive? && (timed_out || TIMEOUT_MARKED_EXIT_CODES.include?(exit_code))
|
|
51
|
+
output = "#{output}\n<command timed out after #{timeout} seconds>"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
ExecResult.new(exit_code:, output:)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def kill(pid)
|
|
61
|
+
Process.kill("KILL", pid)
|
|
62
|
+
rescue Errno::ESRCH
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/miniswen/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: miniswen
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svyatoslav Kryukov
|
|
@@ -55,7 +55,9 @@ files:
|
|
|
55
55
|
- lib/miniswen.rb
|
|
56
56
|
- lib/miniswen/agent.rb
|
|
57
57
|
- lib/miniswen/cli.rb
|
|
58
|
+
- lib/miniswen/cli/reporter.rb
|
|
58
59
|
- lib/miniswen/environment.rb
|
|
60
|
+
- lib/miniswen/environment/docker.rb
|
|
59
61
|
- lib/miniswen/local.rb
|
|
60
62
|
- lib/miniswen/ruby_llm.rb
|
|
61
63
|
- lib/miniswen/testing.rb
|