lemans 0.0.0.pre → 0.2.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +228 -0
  5. data/exe/lemans +17 -0
  6. data/lib/lemans/agents/base.rb +30 -0
  7. data/lib/lemans/agents/miniswen.rb +119 -0
  8. data/lib/lemans/agents/miniswen_installed.rb +67 -0
  9. data/lib/lemans/agents/nop.rb +15 -0
  10. data/lib/lemans/agents/oracle.rb +53 -0
  11. data/lib/lemans/agents.rb +21 -0
  12. data/lib/lemans/bench.rb +280 -0
  13. data/lib/lemans/cli/board_reporter.rb +135 -0
  14. data/lib/lemans/cli/progress_reporter.rb +67 -0
  15. data/lib/lemans/cli.rb +181 -0
  16. data/lib/lemans/clobber.rb +79 -0
  17. data/lib/lemans/environments/base.rb +55 -0
  18. data/lib/lemans/environments/daytona/retries.rb +49 -0
  19. data/lib/lemans/environments/daytona/sdk_tweaks.rb +50 -0
  20. data/lib/lemans/environments/daytona/shell.rb +142 -0
  21. data/lib/lemans/environments/daytona/snapshot_store.rb +163 -0
  22. data/lib/lemans/environments/daytona.rb +175 -0
  23. data/lib/lemans/environments.rb +16 -0
  24. data/lib/lemans/network_policy.rb +66 -0
  25. data/lib/lemans/patch.rb +70 -0
  26. data/lib/lemans/restore_paths.rb +21 -0
  27. data/lib/lemans/results/aggregate.rb +114 -0
  28. data/lib/lemans/results/cost_source.rb +13 -0
  29. data/lib/lemans/results/outcome.rb +36 -0
  30. data/lib/lemans/results/report.rb +149 -0
  31. data/lib/lemans/results/sorting.rb +24 -0
  32. data/lib/lemans/results/tally.rb +19 -0
  33. data/lib/lemans/results/usage.rb +24 -0
  34. data/lib/lemans/run.rb +152 -0
  35. data/lib/lemans/setup.rb +59 -0
  36. data/lib/lemans/setup_files.rb +36 -0
  37. data/lib/lemans/snapshot.rb +55 -0
  38. data/lib/lemans/task.rb +207 -0
  39. data/lib/lemans/tree_digest.rb +24 -0
  40. data/lib/lemans/trial.rb +187 -0
  41. data/lib/lemans/units.rb +44 -0
  42. data/lib/lemans/verifier/assets/eport-lemans.rb +36 -0
  43. data/lib/lemans/verifier/assets/lemans_minitest_reporter.rb +61 -0
  44. data/lib/lemans/verifier.rb +199 -0
  45. data/lib/lemans/version.rb +5 -0
  46. data/lib/lemans.rb +29 -0
  47. data/lib/miniswen/agent.rb +669 -0
  48. data/lib/miniswen/cli.rb +224 -0
  49. data/lib/miniswen/environment.rb +14 -0
  50. data/lib/miniswen/local.rb +42 -0
  51. data/lib/miniswen/ruby_llm.rb +42 -0
  52. data/lib/miniswen/testing.rb +134 -0
  53. data/lib/miniswen/trajectory.rb +110 -0
  54. data/lib/miniswen/version.rb +5 -0
  55. data/lib/miniswen.rb +48 -0
  56. metadata +160 -7
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "securerandom"
5
+ require "time"
6
+
7
+ module Lemans
8
+ # One task, one agent, one reward. Only what happens inside the agent phase
9
+ # is a statement about the model; everything else is the harness's fault.
10
+ class Trial
11
+ attr_reader :task, :bench, :agent_name, :model, :backend, :dir, :id
12
+
13
+ def initialize(task:, bench:, agent_name:, runs_dir:, model: nil, backend: "daytona")
14
+ @task = task
15
+ @bench = bench
16
+ @agent_name = agent_name
17
+ @model = model
18
+ @backend = backend
19
+ @id = "#{task.name}__#{SecureRandom.alphanumeric(7)}"
20
+ @dir = Pathname(runs_dir).join(model_dir, @id)
21
+ end
22
+
23
+ def run
24
+ logs_dir.mkpath
25
+ started_at = Time.now.utc
26
+ reward = nil
27
+ usage = nil
28
+ outcome = Results::Outcome.new(:completed)
29
+ @phases = {}
30
+ # Declared outside the phase blocks they are assigned in, or `ensure`
31
+ # could not stop a sandbox whose setup raised.
32
+ environment = nil
33
+ snapshot = nil
34
+ patch = nil
35
+
36
+ begin
37
+ agent = Agents.build(agent_name, profile: bench.agent, model: model)
38
+
39
+ phase(:environment_setup) do
40
+ environment = start_environment
41
+ prepare(environment)
42
+
43
+ snapshot = Snapshot.new(environment, bench: bench, task: task,
44
+ timeout: bench.environment.build_timeout_sec)
45
+ snapshot.capture!
46
+
47
+ patch = Patch.new(environment, bench: bench, dir: dir)
48
+ patch.seal!
49
+
50
+ agent.install(environment, task: task)
51
+ environment.network_policy = bench.agent.network
52
+ end
53
+
54
+ agent_result = phase(:agent) do
55
+ in_agent_phase { agent.call(environment, task: task, logs_dir: logs_dir) }
56
+ end
57
+ usage = agent_result.usage
58
+ outcome = over_ceiling(agent_result) || agent_result.outcome
59
+
60
+ patch.collect!
61
+
62
+ if outcome.scored?
63
+ phase(:verifier) do
64
+ # The sandbox is sealed before the tests arrive
65
+ environment.network_policy = NetworkPolicy.none
66
+ reward = Verifier.new(bench: bench, task: task, dir: dir, snapshot: snapshot).call(environment)
67
+ end
68
+ end
69
+ rescue VerifierError => e
70
+ outcome = Results::Outcome.new(:verifier_error, detail: e.message)
71
+ rescue ::Miniswen::AccountingError => e
72
+ outcome = Results::Outcome.new(:accounting_error, detail: e.message)
73
+ rescue InfrastructureError, ::Miniswen::InfrastructureError => e
74
+ outcome = Results::Outcome.new(@agent_phase ? :agent_error : :environment_error, detail: e.message)
75
+ rescue ConfigError
76
+ # A malformed bench is the author's bug to fix - raise!
77
+ raise
78
+ rescue StandardError => e
79
+ # A harness bug must leave evidence.
80
+ outcome = Results::Outcome.new(:harness_crash, detail: crash_detail(e))
81
+ ensure
82
+ environment&.stop
83
+ end
84
+
85
+ write_result(started_at: started_at, reward: reward, outcome: outcome, usage: usage)
86
+ rescue SystemCallError, JSON::GeneratorError => e
87
+ # runs_dir unwritable, disk full
88
+ raise ConfigError, "cannot record trial #{id}: #{e.message}"
89
+ end
90
+
91
+ private
92
+
93
+ def model_dir
94
+ (model || bench.agent.model || agent_name).to_s.split("/").last
95
+ end
96
+
97
+ def logs_dir = dir
98
+
99
+ def over_ceiling(result)
100
+ limit = bench.agent.cost_limit
101
+ cost = result.usage&.cost_usd
102
+ return nil unless limit && cost && result.outcome.scored? && cost > limit
103
+
104
+ Results::Outcome.new(:cost_ceiling_reached,
105
+ detail: format("spent $%<cost>.4f against a $%<limit>.4f limit", cost: cost, limit: limit))
106
+ end
107
+
108
+ def crash_detail(error)
109
+ ["#{error.class}: #{error.message}", *Array(error.backtrace).first(5)].join("\n")
110
+ end
111
+
112
+ def phase(name)
113
+ @phases[name] = { started_at: Time.now.utc.iso8601(6) }
114
+ yield
115
+ ensure
116
+ @phases[name][:finished_at] = Time.now.utc.iso8601(6)
117
+ end
118
+
119
+ def in_agent_phase
120
+ @agent_phase = true
121
+ result = yield
122
+ @agent_phase = false
123
+ result
124
+ end
125
+
126
+ def start_environment
127
+ Environments.build(
128
+ backend,
129
+ image: task.environment_image,
130
+ resources: bench.environment.resources,
131
+ network: bench.environment.network,
132
+ build_timeout_sec: bench.environment.build_timeout_sec,
133
+ labels: { "lemans.task" => task.name, "lemans.trial" => id, "lemans.phase" => "agent" }
134
+ ).start
135
+ end
136
+
137
+ def prepare(environment)
138
+ Setup.new(
139
+ commands: bench.environment.setup,
140
+ task: task,
141
+ phase: :environment,
142
+ timeout_sec: bench.environment.build_timeout_sec
143
+ ).call(environment)
144
+ end
145
+
146
+ def write_result(started_at:, reward:, outcome:, usage:)
147
+ finished_at = Time.now.utc
148
+ result = {
149
+ trial: id,
150
+ task: task.name,
151
+ agent: agent_name,
152
+ model: model || bench.agent.model,
153
+ reward: outcome.scored? ? reward : nil,
154
+ outcome: outcome.to_h,
155
+ usage: usage&.to_h,
156
+ lemans_version: VERSION,
157
+ # The digest already hashes the bench's shipped files along with its
158
+ # config, so the per-file listing added bulk, not pinning.
159
+ profile_digest: bench.digest,
160
+ task_digest: task.digest,
161
+ bench: bench.revision.to_h,
162
+ started_at: started_at.iso8601,
163
+ finished_at: finished_at.iso8601,
164
+ duration_sec: (finished_at - started_at).round(1),
165
+ # Where the wall clock went: without this, a slow sandbox morning
166
+ # reads as a slow model.
167
+ phases: @phases,
168
+ tags: task.tags,
169
+ metadata: task.metadata
170
+ }
171
+ atomic_write(result_path, "#{JSON.pretty_generate(result)}\n")
172
+ result
173
+ end
174
+
175
+ def result_path = dir.join("result.json")
176
+
177
+ # --resume treats any result.json as a finished attempt, so the write must
178
+ # be atomic: a rename is either all there or not there at all.
179
+ def atomic_write(path, content)
180
+ tmp = path.dirname.join(".#{path.basename}.#{Process.pid}.#{SecureRandom.hex(4)}")
181
+ tmp.write(content)
182
+ tmp.rename(path)
183
+ ensure
184
+ tmp&.delete if tmp&.exist?
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lemans
4
+ # Durations and sizes as a human writes them ("30m", "2GB"), stored as
5
+ # seconds and megabytes. A bare number gets the obvious reading.
6
+ module Units
7
+ DURATION = /\A(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?\z/
8
+ DURATION_FACTORS = { "ms" => 0.001, "s" => 1, "m" => 60, "h" => 3600, "d" => 86_400 }.freeze
9
+
10
+ SIZE = /\A(\d+(?:\.\d+)?)\s*(MB|GB|TB)?\z/i
11
+ SIZE_FACTORS = { "mb" => 1, "gb" => 1024, "tb" => 1024 * 1024 }.freeze
12
+
13
+ class << self
14
+ def seconds(value, field:)
15
+ return nil if value.nil?
16
+ return finite_nonnegative(value, field, "a duration") if value.is_a?(Numeric)
17
+
18
+ match = DURATION.match(value.to_s.strip)
19
+ raise ConfigError, "#{field}: cannot read #{value.inspect} as a duration (try 30m, 300s, 1h)" unless match
20
+
21
+ match[1].to_f * DURATION_FACTORS.fetch(match[2] || "s")
22
+ end
23
+
24
+ def megabytes(value, field:)
25
+ return nil if value.nil?
26
+ return finite_nonnegative(value, field, "a size").round if value.is_a?(Numeric)
27
+
28
+ match = SIZE.match(value.to_s.strip)
29
+ raise ConfigError, "#{field}: cannot read #{value.inspect} as a size (try 2GB, 512MB)" unless match
30
+
31
+ (match[1].to_f * SIZE_FACTORS.fetch((match[2] || "mb").downcase)).round
32
+ end
33
+
34
+ private
35
+
36
+ def finite_nonnegative(value, field, noun)
37
+ number = value.to_f
38
+ raise ConfigError, "#{field}: cannot read #{value.inspect} as #{noun}" if number.negative? || !number.finite?
39
+
40
+ number
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Loaded when a verifier command opts in with `ruby -report-lemans …`
4
+ # (that is `-r eport-lemans`, resolved from /tests on the LOAD_PATH).
5
+ module LemansReport
6
+ def self.registered? = @registered
7
+
8
+ def self.register
9
+ return if @registered
10
+ return unless defined?(::Minitest) && ::Minitest.respond_to?(:extensions)
11
+
12
+ @registered = true
13
+ require_relative "lemans_minitest_reporter"
14
+ ::Minitest.singleton_class.define_method(:plugin_lemans_report_init) do |_options|
15
+ dir = ENV["LOGS"]
16
+ reporter << Reporter.new(dir) if dir && File.directory?(dir)
17
+ end
18
+ (::Minitest.extensions ||= []) << "lemans_report"
19
+ end
20
+ end
21
+
22
+ if defined?(Minitest)
23
+ LemansReport.register
24
+ else
25
+ # `-r` runs before bundler picks the app's minitest, so requiring minitest
26
+ # here would activate the wrong version. Instead watch class definitions and
27
+ # register the moment minitest's own module body closes; the probe disarms
28
+ # itself and nothing foreign is patched.
29
+ trace = TracePoint.new(:end) do |event|
30
+ next unless event.self.is_a?(Module) && event.self.name == "Minitest"
31
+
32
+ LemansReport.register
33
+ trace.disable if LemansReport.registered?
34
+ end
35
+ trace.enable
36
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module LemansReport
6
+ # Appends every Minitest result to $LOGS/checks.json. Required by
7
+ # eport-lemans once Minitest is loaded; never load this file directly.
8
+ class Reporter < Minitest::AbstractReporter
9
+ def initialize(dir)
10
+ super()
11
+ @dir = dir
12
+ @results = []
13
+ end
14
+
15
+ def record(result)
16
+ @results << result
17
+ end
18
+
19
+ def report
20
+ graded = @results.select { graded?(_1) }
21
+ prior = existing.fetch("checks", {})
22
+ return if graded.empty? && prior.empty?
23
+
24
+ checks = prior.merge(graded.to_h { [name(_1), status(_1)] }).sort.to_h
25
+ File.write(
26
+ File.join(@dir, "checks.json"),
27
+ JSON.pretty_generate(checks: checks, failures: checks.reject { |_, status| status == "pass" }.keys)
28
+ )
29
+ end
30
+
31
+ # A skip inside the harness-shipped tests is an unverified requirement and
32
+ # fails the run. The app's own suite keeps vanilla skip semantics.
33
+ def passed?
34
+ @results.none? { |result| graded?(result) && result.skipped? }
35
+ end
36
+
37
+ private
38
+
39
+ def graded?(result)
40
+ dir = ENV["TESTS"]
41
+ # The trailing slash matters: /testsuite must not count as /tests.
42
+ dir && result.source_location.first.to_s.start_with?("#{dir.chomp("/")}/")
43
+ end
44
+
45
+ def existing
46
+ JSON.parse(File.read(File.join(@dir, "checks.json")))
47
+ rescue StandardError
48
+ {}
49
+ end
50
+
51
+ def name(result) = "#{result.klass}##{result.name}"
52
+
53
+ def status(result)
54
+ if result.skipped? then "skip"
55
+ elsif result.error? then "error"
56
+ elsif result.passed? then "pass"
57
+ else "fail"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+ require "shellwords"
6
+ require "tmpdir"
7
+
8
+ module Lemans
9
+ # Verifies a trial in the sandbox the agent worked in, after Trial has closed
10
+ # its network. The tests are uploaded fresh at verification time, never before.
11
+ class Verifier
12
+ REWARD_RANGE = (0.0..1.0)
13
+
14
+ # Where the task's tests land at verification time
15
+ TESTS_DIR = "/tests"
16
+
17
+ # Harness-owned files used in verification tests
18
+ ASSETS = Pathname(File.expand_path("verifier/assets", __dir__))
19
+
20
+ VERIFY_BIN = "verify"
21
+
22
+ # The message a person finds where the suite output would have been.
23
+ TAMPERED = "The graded surfaces could not be restored from the sealed baseline: the sandbox no " \
24
+ "longer holds the tree sealed before the agent's first turn. Removing or rewriting " \
25
+ "it is a failed check, so this run scores 0.\n"
26
+
27
+ def initialize(bench:, task:, dir:, snapshot: nil)
28
+ @bench = bench
29
+ @task = task
30
+ @dir = dir
31
+ @snapshot = snapshot
32
+ end
33
+
34
+ def call(environment)
35
+ upload_tests(environment)
36
+ prepare(environment)
37
+
38
+ # A baseline the agent made unrestorable is a verdict, not an error.
39
+ unless restore_baseline(environment)
40
+ dir.mkpath
41
+ dir.join("verifier.log").write(TAMPERED)
42
+ return 0.0
43
+ end
44
+
45
+ reward = verify(environment)
46
+ download_evidence(environment)
47
+ reward
48
+ rescue InfrastructureError => e
49
+ salvage_evidence(environment)
50
+ # Everything under verification is the verifier's failure, never the
51
+ # model's
52
+ raise if e.is_a?(VerifierError)
53
+
54
+ raise VerifierError, e.message
55
+ rescue StandardError
56
+ salvage_evidence(environment)
57
+ raise
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :bench, :task, :dir
63
+
64
+ def restore_baseline(environment)
65
+ snapshot = @snapshot || Snapshot.new(environment, bench: bench, task: task,
66
+ timeout: bench.verifier.timeout_sec)
67
+ snapshot.restore!
68
+ end
69
+
70
+ def upload_tests(environment)
71
+ environment.exec!("rm -rf #{TESTS_DIR} && mkdir -p #{TESTS_DIR}")
72
+
73
+ uploads = bench.verification_files.to_h { |local, remote| [remote, local] }
74
+ .merge(task.test_files.to_h { |local, remote| [remote, local] })
75
+
76
+ uploads.each { |remote, local| environment.upload(local, "#{TESTS_DIR}/#{remote}") }
77
+ ASSETS.glob("*.rb").each { |asset| environment.upload(asset, "#{TESTS_DIR}/#{asset.basename}") }
78
+ environment.exec!("chmod +x #{TESTS_DIR}/#{VERIFY_BIN}") if uploads.key?(VERIFY_BIN)
79
+ end
80
+
81
+ def prepare(environment)
82
+ Setup.new(
83
+ commands: bench.verifier.setup,
84
+ task: task,
85
+ phase: :verifier,
86
+ timeout_sec: bench.verifier.timeout_sec
87
+ ).call(environment)
88
+ end
89
+
90
+ def verify(environment)
91
+ # Ensure $LOGS exists
92
+ environment.exec!("mkdir -p #{Shellwords.escape(bench.verifier.logs_dir)}")
93
+ # Ensure the agent hasn't pre-written reward.txt or checks.json
94
+ environment.exec!("rm -f #{Shellwords.escape(bench.verifier.reward_path)} " \
95
+ "#{Shellwords.escape(File.join(bench.verifier.logs_dir, "checks.json"))}")
96
+
97
+ env = { "WORKDIR" => bench.environment.workdir,
98
+ "TESTS" => TESTS_DIR,
99
+ "LOGS" => bench.verifier.logs_dir }
100
+ command = "cd #{Shellwords.escape(bench.environment.workdir)} && " \
101
+ "export RUBYOPT=\"${RUBYOPT:+$RUBYOPT }-I#{TESTS_DIR}\" && " \
102
+ "#{verifier_script}"
103
+ result = environment.exec(command, timeout: bench.verifier.timeout_sec, env: env)
104
+ dir.mkpath
105
+ dir.join("verifier.log").write(result.output.to_s)
106
+
107
+ read_reward(environment, result)
108
+ end
109
+
110
+ def verifier_script
111
+ [bench.verifier.preverify, bench.verifier.command].compact.map { "( #{_1} )" }.join(" && ")
112
+ end
113
+
114
+ def read_reward(environment, command_result)
115
+ reward_path = bench.verifier.reward_path
116
+ present = environment.exec("test -e #{Shellwords.escape(reward_path)}")
117
+ return reward_from_exit(command_result) unless present.success?
118
+
119
+ result = environment.exec("cat #{Shellwords.escape(reward_path)}")
120
+ raise VerifierError, "could not read #{reward_path}: #{result.output.to_s[0, 500]}" unless result.success?
121
+
122
+ value = begin
123
+ Float(result.output.to_s.strip)
124
+ rescue ArgumentError
125
+ raise VerifierError, "verifier wrote #{result.output.to_s.strip.inspect}, which is not a reward"
126
+ end
127
+ raise VerifierError, "verifier wrote a non-finite reward" unless value.finite?
128
+ raise VerifierError, "reward #{value} is outside #{REWARD_RANGE}" unless REWARD_RANGE.cover?(value)
129
+
130
+ value
131
+ end
132
+
133
+ def reward_from_exit(command_result)
134
+ case command_result.exit_code
135
+ when 0 then 1.0
136
+ when 1 then 0.0
137
+ else
138
+ raise VerifierError,
139
+ "verifier exited #{command_result.exit_code} and wrote no reward to #{bench.verifier.reward_path}"
140
+ end
141
+ end
142
+
143
+ def download_evidence(environment)
144
+ @evidence_attempted = true
145
+ root = bench.verifier.logs_dir
146
+ paths = list_files(environment, root)
147
+ return if paths.empty?
148
+
149
+ relative_to = Pathname(root).cleanpath
150
+
151
+ # Use a temp dir to download evidence to check for collisions
152
+ Dir.mktmpdir("lemans-evidence") do |staging|
153
+ paths.each do |remote|
154
+ relative = checked_remote_path(remote, root).relative_path_from(relative_to)
155
+ staged = Pathname(staging).join(relative)
156
+ staged.dirname.mkpath
157
+ environment.download(remote, staged)
158
+
159
+ destination = dir.join(relative)
160
+ if destination.exist?
161
+ warn "lemans: evidence file #{relative} collides with a harness file and was dropped"
162
+ next
163
+ end
164
+
165
+ destination.dirname.mkpath
166
+ FileUtils.cp(staged, destination)
167
+ end
168
+ end
169
+ end
170
+
171
+ def list_files(environment, declared)
172
+ return [] unless environment.exec("test -d #{Shellwords.escape(declared)}").success?
173
+
174
+ listing = environment.exec("find #{Shellwords.escape(declared)} -type f -print0")
175
+ raise VerifierError, "could not list #{declared}: #{listing.output.to_s[0, 500]}" unless listing.success?
176
+
177
+ listing.output.to_s.split("\0").reject(&:empty?)
178
+ end
179
+
180
+ def checked_remote_path(remote, declared)
181
+ root = Pathname(declared).cleanpath
182
+ candidate = Pathname(remote).cleanpath
183
+
184
+ raise VerifierError, "evidence file #{remote.inspect} escapes #{declared}" unless candidate.absolute? && candidate.to_s.start_with?("#{root}/")
185
+
186
+ raise VerifierError, "evidence file #{remote.inspect} contains control characters" if remote.match?(/[[:cntrl:]]/)
187
+
188
+ candidate
189
+ end
190
+
191
+ def salvage_evidence(environment)
192
+ return if @evidence_attempted
193
+
194
+ download_evidence(environment)
195
+ rescue StandardError => e
196
+ warn "lemans: could not save the verifier's evidence: #{e.class}: #{e.message}"
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lemans
4
+ VERSION = "0.2.0"
5
+ end
data/lib/lemans.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ loader = Zeitwerk::Loader.for_gem
6
+ loader.inflector.inflect("cli" => "CLI", "atif" => "ATIF")
7
+ # miniswen lives in this repo but is a plain-require gem of its own.
8
+ loader.ignore("#{__dir__}/miniswen.rb", "#{__dir__}/miniswen")
9
+ # assets/ holds files uploaded into sandboxes, not Ruby the harness loads.
10
+ loader.ignore("#{__dir__}/lemans/verifier/assets")
11
+ loader.setup
12
+
13
+ require "miniswen"
14
+
15
+ module Lemans
16
+ class Error < StandardError; end
17
+
18
+ # Anything the caller can fix: a malformed bench.yml, a task directory
19
+ # missing a file, an unknown network mode.
20
+ class ConfigError < Error; end
21
+
22
+ # A backend, agent, or verifier failing in a way that is the harness's fault
23
+ # rather than the model's. These become an Outcome, never a zero reward.
24
+ class InfrastructureError < Error; end
25
+
26
+ # The verifier itself failed, or wrote something that is not a reward. Retrying
27
+ # would verify the same patch the same way, so it is terminal.
28
+ class VerifierError < InfrastructureError; end
29
+ end