llm-experiment 0.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 +7 -0
- data/CHANGELOG.md +40 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +190 -0
- data/exe/llmx +6 -0
- data/lib/llm-experiment.rb +3 -0
- data/lib/llm_experiment/auth.rb +143 -0
- data/lib/llm_experiment/cleaner.rb +183 -0
- data/lib/llm_experiment/cli/build_command.rb +37 -0
- data/lib/llm_experiment/cli/clean_command.rb +42 -0
- data/lib/llm_experiment/cli/doctor_command.rb +30 -0
- data/lib/llm_experiment/cli/login_command.rb +25 -0
- data/lib/llm_experiment/cli/metrics_command.rb +32 -0
- data/lib/llm_experiment/cli/new_command.rb +21 -0
- data/lib/llm_experiment/cli/parse_command.rb +77 -0
- data/lib/llm_experiment/cli/run_command.rb +74 -0
- data/lib/llm_experiment/cli/sanitize_command.rb +34 -0
- data/lib/llm_experiment/cli/shell_command.rb +35 -0
- data/lib/llm_experiment/cli/status_command.rb +69 -0
- data/lib/llm_experiment/cli/version_command.rb +19 -0
- data/lib/llm_experiment/cli.rb +120 -0
- data/lib/llm_experiment/container.rb +178 -0
- data/lib/llm_experiment/doctor.rb +126 -0
- data/lib/llm_experiment/experiment.rb +163 -0
- data/lib/llm_experiment/grid.rb +117 -0
- data/lib/llm_experiment/image_builder/app.rb +267 -0
- data/lib/llm_experiment/image_builder/base.rb +64 -0
- data/lib/llm_experiment/metrics_report.rb +138 -0
- data/lib/llm_experiment/pins.rb +22 -0
- data/lib/llm_experiment/sanitizer.rb +144 -0
- data/lib/llm_experiment/scaffold.rb +43 -0
- data/lib/llm_experiment/shell.rb +60 -0
- data/lib/llm_experiment/stats.rb +69 -0
- data/lib/llm_experiment/transcript/claude.rb +104 -0
- data/lib/llm_experiment/transcript/codex.rb +96 -0
- data/lib/llm_experiment/transcript/hermeticity.rb +35 -0
- data/lib/llm_experiment/transcript/parser.rb +105 -0
- data/lib/llm_experiment/transcript.rb +27 -0
- data/lib/llm_experiment/trial.rb +204 -0
- data/lib/llm_experiment/version.rb +5 -0
- data/lib/llm_experiment.rb +70 -0
- data/templates/README.md.erb +26 -0
- data/templates/base.Containerfile +120 -0
- data/templates/experiment.yml.erb +30 -0
- data/templates/gitignore +3 -0
- data/templates/runner.rb +305 -0
- metadata +92 -0
data/templates/runner.rb
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Runs inside the trial container. `llmx run` copies this in and starts it;
|
|
5
|
+
# it is never executed on the host.
|
|
6
|
+
#
|
|
7
|
+
# Order matters here. Everything that could tell the agent where the defect is
|
|
8
|
+
# has to be gone before the agent starts, and the test has to be observed
|
|
9
|
+
# failing before the agent starts, so a mis-planted bug is caught as a bad trial
|
|
10
|
+
# instead of being scored as an instant fix.
|
|
11
|
+
|
|
12
|
+
require "json"
|
|
13
|
+
require "open3"
|
|
14
|
+
require "fileutils"
|
|
15
|
+
|
|
16
|
+
APP_DIR = "/workspace/app"
|
|
17
|
+
RESULTS = "/results"
|
|
18
|
+
|
|
19
|
+
def cfg(key, default = nil)
|
|
20
|
+
value = ENV["LLMX_#{key.to_s.upcase}"]
|
|
21
|
+
value.nil? || value.empty? ? default : value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run(cmd, dir: APP_DIR, timeout: nil, env: {})
|
|
25
|
+
cmd = "timeout #{timeout} #{cmd}" if timeout
|
|
26
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
27
|
+
out, err, status = Open3.capture3(env, cmd, chdir: dir)
|
|
28
|
+
{
|
|
29
|
+
"cmd" => cmd,
|
|
30
|
+
"exit" => status.exitstatus,
|
|
31
|
+
"stdout" => out,
|
|
32
|
+
"stderr" => err,
|
|
33
|
+
"seconds" => (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).round(3)
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def log(msg)
|
|
38
|
+
warn "[runner] #{msg}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
branch = cfg(:branch) or abort "LLMX_BRANCH required"
|
|
42
|
+
agent = cfg(:agent) or abort "LLMX_AGENT required"
|
|
43
|
+
condition = cfg(:condition) or abort "LLMX_CONDITION required"
|
|
44
|
+
test_file = cfg(:test_file) or abort "LLMX_TEST_FILE required"
|
|
45
|
+
prompt_b64 = cfg(:prompt_b64) or abort "LLMX_PROMPT_B64 required"
|
|
46
|
+
impl_files = (cfg(:impl_files) || "").split(",")
|
|
47
|
+
model = cfg(:model)
|
|
48
|
+
db_prepare = cfg(:db_prepare, "bin/rails db:test:prepare")
|
|
49
|
+
timeout_s = cfg(:timeout_seconds, "900").to_i
|
|
50
|
+
database = cfg(:database, "sqlite3")
|
|
51
|
+
test_command = cfg(:test_command, "bin/rails test")
|
|
52
|
+
suite_ran_pattern = Regexp.new(cfg(:suite_ran_pattern, '\d+ runs?, \d+ assertions?'))
|
|
53
|
+
|
|
54
|
+
# unpack1("m0") is strict_decode64 without the base64 gem, which Ruby 3.4
|
|
55
|
+
# unbundled. This script runs inside a container against whichever ruby the
|
|
56
|
+
# app pins, so it leans on core only and requires nothing that could be absent.
|
|
57
|
+
prompt = prompt_b64.unpack1("m0")
|
|
58
|
+
|
|
59
|
+
meta = {
|
|
60
|
+
"task_id" => cfg(:task_id),
|
|
61
|
+
"app" => cfg(:app),
|
|
62
|
+
"agent" => agent,
|
|
63
|
+
"condition" => condition,
|
|
64
|
+
"branch" => branch,
|
|
65
|
+
"model_requested" => model,
|
|
66
|
+
# Persisted because parse_transcript.rb needs them to compute
|
|
67
|
+
# tool_calls_to_first_defect_read. Without them it matches against an empty
|
|
68
|
+
# list, and the metric comes out nil on every trial while looking like a
|
|
69
|
+
# null observation rather than a broken one.
|
|
70
|
+
"impl_files" => impl_files,
|
|
71
|
+
"test_file" => test_file,
|
|
72
|
+
"started_at" => Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# --- Postgres, if this app needs it -----------------------------------------
|
|
76
|
+
#
|
|
77
|
+
# The cluster is the agent user's own, created at image build, so starting it
|
|
78
|
+
# needs no privileges. The packaged system cluster would need root, and root is
|
|
79
|
+
# not something to hand a container that runs model-authored shell commands.
|
|
80
|
+
if database == "postgresql"
|
|
81
|
+
log "starting postgres"
|
|
82
|
+
pgbin = File.dirname(Dir.glob("/usr/lib/postgresql/*/bin/pg_ctl").first)
|
|
83
|
+
pgdata = ENV.fetch("PGDATA", "/home/agent/pgdata")
|
|
84
|
+
system("#{pgbin}/pg_ctl -D #{pgdata} -l /tmp/postgres.log -o '-p 5432' -w start > /dev/null 2>&1")
|
|
85
|
+
ready = false
|
|
86
|
+
30.times do
|
|
87
|
+
ready = system("#{pgbin}/pg_isready -q -h 127.0.0.1 -p 5432")
|
|
88
|
+
break if ready
|
|
89
|
+
|
|
90
|
+
sleep 0.5
|
|
91
|
+
end
|
|
92
|
+
meta["postgres_ready"] = ready
|
|
93
|
+
log "postgres ready: #{ready}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# --- Isolate the branch under test ------------------------------------------
|
|
97
|
+
#
|
|
98
|
+
# The image ships every trial branch. Leaving them in place would let one
|
|
99
|
+
# `git diff trial/base` show the planted change, which is the answer. Delete
|
|
100
|
+
# everything except the branch being tested, then drop the objects so the trees
|
|
101
|
+
# are not merely unreferenced but gone.
|
|
102
|
+
|
|
103
|
+
# The format string is quoted because `run` hands a single string to /bin/sh:
|
|
104
|
+
# unquoted, sh reads `%(refname:short)` as a subshell, dies with a syntax
|
|
105
|
+
# error and returns empty stdout. `run` ignores exit codes, so the deletion
|
|
106
|
+
# loop then had nothing to iterate and silently deleted nothing, while
|
|
107
|
+
# branches_visible_to_agent recorded [] and read like a success.
|
|
108
|
+
BRANCH_LIST = "git branch --format='%(refname:short)'"
|
|
109
|
+
|
|
110
|
+
log "isolating #{branch}"
|
|
111
|
+
run("git checkout -q #{branch}")
|
|
112
|
+
others = run(BRANCH_LIST)["stdout"].split("\n").map(&:strip)
|
|
113
|
+
.reject { |b| b.empty? || b == branch }
|
|
114
|
+
others.each { |b| run("git branch -q -D #{b}") }
|
|
115
|
+
run("git reflog expire --expire=now --all")
|
|
116
|
+
run("git gc --prune=now --quiet")
|
|
117
|
+
|
|
118
|
+
visible = run("git log --all --oneline")["stdout"].strip
|
|
119
|
+
meta["history_visible_to_agent"] = visible
|
|
120
|
+
meta["branches_visible_to_agent"] = run(BRANCH_LIST)["stdout"].split("\n").map(&:strip)
|
|
121
|
+
|
|
122
|
+
# Isolation is the guarantee the whole experiment rests on, and every command
|
|
123
|
+
# above ignores its exit code, so it is checked rather than assumed. A trial
|
|
124
|
+
# whose repository still holds the other branches is not merely imperfect: one
|
|
125
|
+
# `git diff trial/base` shows the planted change, so the trial is worthless and
|
|
126
|
+
# must not be scored.
|
|
127
|
+
leftover = meta["branches_visible_to_agent"] - [branch]
|
|
128
|
+
if !leftover.empty? || visible.lines.size != 1
|
|
129
|
+
log "aborting: isolation failed (branches left: #{leftover.inspect}, " \
|
|
130
|
+
"#{visible.lines.size} commit(s) visible)"
|
|
131
|
+
meta["aborted"] = "isolation_failed"
|
|
132
|
+
File.write(File.join(RESULTS, "meta.json"), JSON.pretty_generate(meta))
|
|
133
|
+
exit 4
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
dirty = run("git status --porcelain")["stdout"].strip
|
|
137
|
+
meta["clean_tree_before"] = dirty.empty?
|
|
138
|
+
log "tree clean before agent: #{dirty.empty?}"
|
|
139
|
+
|
|
140
|
+
# --- Database ----------------------------------------------------------------
|
|
141
|
+
prep = run(db_prepare, timeout: 300)
|
|
142
|
+
meta["db_prepare_exit"] = prep["exit"]
|
|
143
|
+
File.write(File.join(RESULTS, "db_prepare.txt"), prep["stdout"] + prep["stderr"])
|
|
144
|
+
|
|
145
|
+
# --- Confirm the bug is real before spending a trial on it -------------------
|
|
146
|
+
#
|
|
147
|
+
# A non-zero exit is not enough. An app that cannot boot also exits non-zero,
|
|
148
|
+
# and scoring that as "the planted bug reproduces" would spend a trial on a
|
|
149
|
+
# broken container and then read the agent's confusion as data. Require a
|
|
150
|
+
# Minitest summary line, which only appears if the suite actually ran.
|
|
151
|
+
before = run("#{test_command} #{test_file}", timeout: 600)
|
|
152
|
+
output = before["stdout"] + before["stderr"]
|
|
153
|
+
meta["test_before"] = { "exit" => before["exit"], "seconds" => before["seconds"] }
|
|
154
|
+
File.write(File.join(RESULTS, "test_before.txt"), output)
|
|
155
|
+
|
|
156
|
+
suite_ran = output.match?(suite_ran_pattern)
|
|
157
|
+
meta["suite_ran_before"] = suite_ran
|
|
158
|
+
meta["task_reproduces"] = suite_ran && before["exit"] != 0
|
|
159
|
+
|
|
160
|
+
unless meta["task_reproduces"]
|
|
161
|
+
reason = suite_ran ? "task_did_not_reproduce" : "suite_did_not_run"
|
|
162
|
+
log "aborting: #{reason} (see test_before.txt)"
|
|
163
|
+
meta["aborted"] = reason
|
|
164
|
+
File.write(File.join(RESULTS, "meta.json"), JSON.pretty_generate(meta))
|
|
165
|
+
exit 3
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# --- Give the agent a private config directory -------------------------------
|
|
169
|
+
#
|
|
170
|
+
# CLAUDE_CONFIG_DIR points at the mounted credential store, and Claude Code
|
|
171
|
+
# derives its project state and auto-memory directory from it: the init event
|
|
172
|
+
# reports memory at <config>/projects/-workspace-app/memory/. That directory is
|
|
173
|
+
# writable, shared by every trial and persisted on the host, which opens three
|
|
174
|
+
# channels between trials that are supposed to be independent:
|
|
175
|
+
#
|
|
176
|
+
# - session transcripts accumulate under projects/
|
|
177
|
+
# - .claude.json records per-project onboarding and startup state, so the
|
|
178
|
+
# first trial of a grid does not start from the same place as the rest
|
|
179
|
+
# - anything written to the memory directory in one trial is read by the
|
|
180
|
+
# next, and in this experiment that could be the location of the defect
|
|
181
|
+
#
|
|
182
|
+
# So each trial copies the credentials into its own directory and points the
|
|
183
|
+
# CLI at the copy. The copy lives in the container and dies with it; the
|
|
184
|
+
# mounted store is only ever read, which keeps every trial starting from
|
|
185
|
+
# byte-identical state. Codex already has this guarantee from
|
|
186
|
+
# --ignore-user-config --ephemeral.
|
|
187
|
+
SEEDED = %w[.credentials.json .claude.json settings.json auth.json config.toml].freeze
|
|
188
|
+
|
|
189
|
+
def private_config(source, dest)
|
|
190
|
+
return nil unless source && Dir.exist?(source)
|
|
191
|
+
|
|
192
|
+
FileUtils.rm_rf(dest)
|
|
193
|
+
FileUtils.mkdir_p(dest)
|
|
194
|
+
SEEDED.each do |f|
|
|
195
|
+
from = File.join(source, f)
|
|
196
|
+
FileUtils.cp(from, File.join(dest, f)) if File.exist?(from)
|
|
197
|
+
end
|
|
198
|
+
dest
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if agent == "claude" && (dir = private_config(ENV["CLAUDE_CONFIG_DIR"], "/home/agent/.claude-trial"))
|
|
202
|
+
ENV["CLAUDE_CONFIG_DIR"] = dir
|
|
203
|
+
end
|
|
204
|
+
if agent == "codex" && (dir = private_config(ENV["CODEX_HOME"], "/home/agent/.codex-trial"))
|
|
205
|
+
ENV["CODEX_HOME"] = dir
|
|
206
|
+
end
|
|
207
|
+
meta["config_dir"] = agent == "claude" ? ENV["CLAUDE_CONFIG_DIR"] : ENV["CODEX_HOME"]
|
|
208
|
+
|
|
209
|
+
# --- Run the agent -----------------------------------------------------------
|
|
210
|
+
transcript = File.join(RESULTS, "transcript.jsonl")
|
|
211
|
+
|
|
212
|
+
cmd =
|
|
213
|
+
case agent
|
|
214
|
+
when "claude"
|
|
215
|
+
base = ["claude", "-p", "--output-format", "stream-json", "--verbose",
|
|
216
|
+
"--permission-mode", "bypassPermissions"]
|
|
217
|
+
base += ["--model", model] if model
|
|
218
|
+
base
|
|
219
|
+
when "codex"
|
|
220
|
+
base = ["codex", "exec", "--json",
|
|
221
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
222
|
+
"--skip-git-repo-check", "--ignore-user-config", "--ephemeral",
|
|
223
|
+
"-C", APP_DIR]
|
|
224
|
+
base += ["--model", model] if model
|
|
225
|
+
base << "-"
|
|
226
|
+
else
|
|
227
|
+
abort "unknown agent #{agent}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
meta["agent_argv"] = cmd
|
|
231
|
+
log "running #{cmd.join(" ")}"
|
|
232
|
+
|
|
233
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
234
|
+
agent_exit = nil
|
|
235
|
+
timed_out = false
|
|
236
|
+
|
|
237
|
+
File.open(transcript, "w") do |sink|
|
|
238
|
+
Open3.popen3({}, *cmd, chdir: APP_DIR) do |stdin, stdout, stderr, wait|
|
|
239
|
+
stdin.write(prompt)
|
|
240
|
+
stdin.close
|
|
241
|
+
|
|
242
|
+
err_buf = +""
|
|
243
|
+
err_thread = Thread.new { stderr.each_line { |l| err_buf << l } }
|
|
244
|
+
out_thread = Thread.new do
|
|
245
|
+
stdout.each_line do |line|
|
|
246
|
+
sink.write(line)
|
|
247
|
+
sink.flush
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
unless wait.join(timeout_s)
|
|
252
|
+
timed_out = true
|
|
253
|
+
begin
|
|
254
|
+
Process.kill("KILL", wait.pid)
|
|
255
|
+
rescue StandardError
|
|
256
|
+
nil
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
out_thread.join(10)
|
|
260
|
+
err_thread.join(5)
|
|
261
|
+
agent_exit = begin
|
|
262
|
+
wait.value.exitstatus
|
|
263
|
+
rescue StandardError
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
File.write(File.join(RESULTS, "agent_stderr.txt"), err_buf)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
meta["agent_exit"] = agent_exit
|
|
271
|
+
meta["timed_out"] = timed_out
|
|
272
|
+
meta["wall_seconds"] = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).round(3)
|
|
273
|
+
log "agent finished in #{meta["wall_seconds"]}s (exit #{agent_exit}, timeout #{timed_out})"
|
|
274
|
+
|
|
275
|
+
# --- What did it change? -----------------------------------------------------
|
|
276
|
+
diff = run("git diff")
|
|
277
|
+
File.write(File.join(RESULTS, "agent.diff"), diff["stdout"])
|
|
278
|
+
meta["changed_files"] = run("git diff --name-only")["stdout"].split("\n").map(&:strip).reject(&:empty?)
|
|
279
|
+
meta["untracked_files"] = run("git ls-files --others --exclude-standard")["stdout"]
|
|
280
|
+
.split("\n").map(&:strip).reject(&:empty?)
|
|
281
|
+
meta["touched_impl_file"] = (meta["changed_files"] & impl_files).any?
|
|
282
|
+
meta["edited_test_dir"] = meta["changed_files"].any? { |f| f.start_with?("test/") }
|
|
283
|
+
|
|
284
|
+
# --- Did the fix hold? -------------------------------------------------------
|
|
285
|
+
after = run("#{test_command} #{test_file}", timeout: 600)
|
|
286
|
+
after_output = after["stdout"] + after["stderr"]
|
|
287
|
+
meta["test_after"] = { "exit" => after["exit"], "seconds" => after["seconds"] }
|
|
288
|
+
File.write(File.join(RESULTS, "test_after.txt"), after_output)
|
|
289
|
+
|
|
290
|
+
# A green run only counts if the suite ran and the agent did not simply edit the
|
|
291
|
+
# test until it agreed with the bug.
|
|
292
|
+
meta["fix_verified"] = after["exit"].zero? &&
|
|
293
|
+
after_output.match?(suite_ran_pattern) &&
|
|
294
|
+
!meta["edited_test_dir"]
|
|
295
|
+
|
|
296
|
+
meta["toolchain"] = begin
|
|
297
|
+
JSON.parse(File.read("/etc/llmx-versions.json"))
|
|
298
|
+
rescue StandardError
|
|
299
|
+
nil
|
|
300
|
+
end
|
|
301
|
+
meta["finished_at"] = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
302
|
+
|
|
303
|
+
File.write(File.join(RESULTS, "meta.json"), JSON.pretty_generate(meta))
|
|
304
|
+
log "wrote #{File.join(RESULTS, "meta.json")}"
|
|
305
|
+
exit 0
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: llm-experiment
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Lucian Ghinda
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: 'A clean-room harness for measuring coding agents (Claude Code, Codex
|
|
13
|
+
CLI): fresh Apple containers per trial, pinned toolchains, transcript parsing, exact
|
|
14
|
+
statistics, and a sanitize gate for publishing results.'
|
|
15
|
+
email: dev@ghinda.com
|
|
16
|
+
executables:
|
|
17
|
+
- llmx
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- CODE_OF_CONDUCT.md
|
|
23
|
+
- LICENSE.txt
|
|
24
|
+
- README.md
|
|
25
|
+
- exe/llmx
|
|
26
|
+
- lib/llm-experiment.rb
|
|
27
|
+
- lib/llm_experiment.rb
|
|
28
|
+
- lib/llm_experiment/auth.rb
|
|
29
|
+
- lib/llm_experiment/cleaner.rb
|
|
30
|
+
- lib/llm_experiment/cli.rb
|
|
31
|
+
- lib/llm_experiment/cli/build_command.rb
|
|
32
|
+
- lib/llm_experiment/cli/clean_command.rb
|
|
33
|
+
- lib/llm_experiment/cli/doctor_command.rb
|
|
34
|
+
- lib/llm_experiment/cli/login_command.rb
|
|
35
|
+
- lib/llm_experiment/cli/metrics_command.rb
|
|
36
|
+
- lib/llm_experiment/cli/new_command.rb
|
|
37
|
+
- lib/llm_experiment/cli/parse_command.rb
|
|
38
|
+
- lib/llm_experiment/cli/run_command.rb
|
|
39
|
+
- lib/llm_experiment/cli/sanitize_command.rb
|
|
40
|
+
- lib/llm_experiment/cli/shell_command.rb
|
|
41
|
+
- lib/llm_experiment/cli/status_command.rb
|
|
42
|
+
- lib/llm_experiment/cli/version_command.rb
|
|
43
|
+
- lib/llm_experiment/container.rb
|
|
44
|
+
- lib/llm_experiment/doctor.rb
|
|
45
|
+
- lib/llm_experiment/experiment.rb
|
|
46
|
+
- lib/llm_experiment/grid.rb
|
|
47
|
+
- lib/llm_experiment/image_builder/app.rb
|
|
48
|
+
- lib/llm_experiment/image_builder/base.rb
|
|
49
|
+
- lib/llm_experiment/metrics_report.rb
|
|
50
|
+
- lib/llm_experiment/pins.rb
|
|
51
|
+
- lib/llm_experiment/sanitizer.rb
|
|
52
|
+
- lib/llm_experiment/scaffold.rb
|
|
53
|
+
- lib/llm_experiment/shell.rb
|
|
54
|
+
- lib/llm_experiment/stats.rb
|
|
55
|
+
- lib/llm_experiment/transcript.rb
|
|
56
|
+
- lib/llm_experiment/transcript/claude.rb
|
|
57
|
+
- lib/llm_experiment/transcript/codex.rb
|
|
58
|
+
- lib/llm_experiment/transcript/hermeticity.rb
|
|
59
|
+
- lib/llm_experiment/transcript/parser.rb
|
|
60
|
+
- lib/llm_experiment/trial.rb
|
|
61
|
+
- lib/llm_experiment/version.rb
|
|
62
|
+
- templates/README.md.erb
|
|
63
|
+
- templates/base.Containerfile
|
|
64
|
+
- templates/experiment.yml.erb
|
|
65
|
+
- templates/gitignore
|
|
66
|
+
- templates/runner.rb
|
|
67
|
+
homepage: https://github.com/lucianghinda/llm-experiment
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata:
|
|
71
|
+
rubygems_mfa_required: 'true'
|
|
72
|
+
source_code_uri: https://github.com/lucianghinda/llm-experiment
|
|
73
|
+
changelog_uri: https://github.com/lucianghinda/llm-experiment/blob/main/CHANGELOG.md
|
|
74
|
+
bug_tracker_uri: https://github.com/lucianghinda/llm-experiment/issues
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.4'
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 4.0.11
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: Run controlled, reproducible experiments on coding agents
|
|
92
|
+
test_files: []
|