commitarena 0.1.1__py3-none-any.whl

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.
@@ -0,0 +1,40 @@
1
+ """commitarena: replay real bug fixes from git history against any model, in a sandbox with no network."""
2
+
3
+ __version__ = "0.1.1"
4
+
5
+ from .agent import SYSTEM_PROMPT, AgentConfig # noqa: E402
6
+ from .errors import ( # noqa: E402
7
+ BudgetExceeded,
8
+ CommitArenaError,
9
+ DockerUnavailable,
10
+ MissingAPIKey,
11
+ ModelUnavailable,
12
+ TaskError,
13
+ )
14
+ from .llm import Budget, ModelSpec # noqa: E402
15
+ from .runner import ContextFile, RunResult, run_suite, run_task # noqa: E402
16
+ from .sandbox import EnvSpec, SandboxConfig, ensure_docker # noqa: E402
17
+ from .tasks import Task, load_tasks # noqa: E402
18
+
19
+ __all__ = [
20
+ "SYSTEM_PROMPT",
21
+ "AgentConfig",
22
+ "Budget",
23
+ "BudgetExceeded",
24
+ "CommitArenaError",
25
+ "ContextFile",
26
+ "DockerUnavailable",
27
+ "EnvSpec",
28
+ "MissingAPIKey",
29
+ "ModelSpec",
30
+ "ModelUnavailable",
31
+ "RunResult",
32
+ "SandboxConfig",
33
+ "Task",
34
+ "TaskError",
35
+ "__version__",
36
+ "ensure_docker",
37
+ "load_tasks",
38
+ "run_suite",
39
+ "run_task",
40
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
commitarena/agent.py ADDED
@@ -0,0 +1,205 @@
1
+ """The minimal agent harness: one `bash` tool, same loop and prompt for every model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass, field
9
+
10
+ from .errors import BudgetExceeded
11
+ from .llm import ChatClient, Usage
12
+ from .sandbox import Sandbox
13
+
14
+ SYSTEM_PROMPT = """\
15
+ You are a software engineer fixing a bug in a Python repository. The repository is at /work inside a \
16
+ Linux container with no network access (pip install will not work; the dependencies are already \
17
+ installed). Use the `bash` tool to inspect files, edit them (for example with python, sed or a heredoc) \
18
+ and run tests. Each call runs in a fresh shell starting in /work; long outputs are truncated.
19
+
20
+ Work in small steps: read the failing tests, find the cause in the source code, make a minimal fix, \
21
+ and run the tests. Do not modify test files: they are restored before scoring. When the tests pass, \
22
+ reply with a one-line summary and no tool call to finish."""
23
+
24
+ NUDGE = (
25
+ "Your last reply was empty or cut off at the token limit. Keep your reasoning short and call the bash "
26
+ "tool, or reply with a one-line summary if you are done."
27
+ )
28
+ MAX_NUDGES = 2 # consecutive empty or truncated replies before giving up
29
+
30
+ BASH_TOOL = {
31
+ "type": "function",
32
+ "function": {
33
+ "name": "bash",
34
+ "description": "Run a bash command in /work and return its combined stdout/stderr and exit code.",
35
+ "parameters": {
36
+ "type": "object",
37
+ "properties": {"command": {"type": "string", "description": "The command to run."}},
38
+ "required": ["command"],
39
+ },
40
+ },
41
+ }
42
+
43
+
44
+ @dataclass
45
+ class AgentConfig:
46
+ max_steps: int = 30 # model calls
47
+ max_wall_time: int = 1200 # seconds for the whole attempt
48
+ max_output_chars: int = 6000 # per tool result, head + tail kept
49
+ max_tokens: int = 4096 # per model reply
50
+ temperature: float = 0.0
51
+ system_prompt: str = SYSTEM_PROMPT
52
+ # Older tool outputs are elided once the conversation passes this size, so a 16K-token local
53
+ # model keeps the task text and the latest outputs (about 3.5 characters per token).
54
+ max_context_chars: int = 40000
55
+
56
+
57
+ @dataclass
58
+ class AgentOutcome:
59
+ steps: int = 0
60
+ tool_calls: int = 0
61
+ usage: Usage = field(default_factory=Usage)
62
+ stop_reason: str = "" # finished | max_steps | time_limit | no_action | budget_exceeded | model_error
63
+ error: str = ""
64
+ final_message: str = ""
65
+ trajectory: list[dict] = field(default_factory=list)
66
+
67
+
68
+ def truncate(text: str, limit: int) -> str:
69
+ if len(text) <= limit:
70
+ return text
71
+ half = limit // 2
72
+ return (
73
+ f"{text[:half]}\n... [{len(text) - limit} characters truncated. Use grep -n or sed -n 'START,ENDp' "
74
+ f"FILE to see specific lines] ...\n{text[-half:]}"
75
+ )
76
+
77
+
78
+ ELIDED = "[older output elided to fit the context window]"
79
+ REPEAT_NOTE = "[note: you already ran this exact command and got the same output. Try a different approach.]"
80
+
81
+
82
+ def fit_context(messages: list[dict], max_chars: int, keep_last: int = 4) -> list[dict]:
83
+ """Copy of `messages` with the oldest tool outputs elided until the total fits in `max_chars`.
84
+
85
+ The system prompt, the task and the last `keep_last` messages are never elided."""
86
+ size = sum(len(m.get("content") or "") for m in messages)
87
+ if size <= max_chars:
88
+ return messages
89
+ out = [dict(m) for m in messages]
90
+ for m in out[2 : max(2, len(out) - keep_last)]:
91
+ if size <= max_chars:
92
+ break
93
+ if m["role"] == "tool" and m["content"] != ELIDED:
94
+ size -= len(m["content"]) - len(ELIDED)
95
+ m["content"] = ELIDED
96
+ return out
97
+
98
+
99
+ def message_text(content) -> str:
100
+ """Reply text. Some providers return a list of blocks (e.g. thinking + text); keep the text ones."""
101
+ if content is None:
102
+ return ""
103
+ if isinstance(content, str):
104
+ return content
105
+ parts = []
106
+ for block in content:
107
+ kind = block.get("type") if isinstance(block, dict) else getattr(block, "type", None)
108
+ text = block.get("text") if isinstance(block, dict) else getattr(block, "text", None)
109
+ if kind in (None, "text") and isinstance(text, str):
110
+ parts.append(text)
111
+ return "".join(parts)
112
+
113
+
114
+ def _assistant_dict(msg) -> dict:
115
+ d: dict = {"role": "assistant", "content": message_text(msg.content)}
116
+ if msg.tool_calls:
117
+ d["tool_calls"] = [
118
+ {
119
+ "id": tc.id,
120
+ "type": "function",
121
+ "function": {"name": tc.function.name, "arguments": tc.function.arguments},
122
+ }
123
+ for tc in msg.tool_calls
124
+ ]
125
+ return d
126
+
127
+
128
+ def run_agent(
129
+ client: ChatClient,
130
+ sandbox: Sandbox,
131
+ prompt: str,
132
+ config: AgentConfig | None = None,
133
+ system_extra: str = "",
134
+ on_step: Callable[[int, str], None] | None = None,
135
+ ) -> AgentOutcome:
136
+ cfg = config or AgentConfig()
137
+ system = cfg.system_prompt + (f"\n\n{system_extra}" if system_extra else "")
138
+ messages: list[dict] = [{"role": "system", "content": system}, {"role": "user", "content": prompt}]
139
+ out = AgentOutcome()
140
+ out.trajectory = messages
141
+ start = time.monotonic()
142
+ nudges = 0
143
+ seen: dict[str, str] = {} # command -> last output, to flag exact repeats
144
+ while True:
145
+ if out.steps >= cfg.max_steps:
146
+ out.stop_reason = "max_steps"
147
+ break
148
+ if time.monotonic() - start > cfg.max_wall_time:
149
+ out.stop_reason = "time_limit"
150
+ break
151
+ try:
152
+ resp, usage = client.chat(
153
+ fit_context(messages, cfg.max_context_chars),
154
+ tools=[BASH_TOOL],
155
+ temperature=cfg.temperature,
156
+ top_p=1.0,
157
+ max_tokens=cfg.max_tokens,
158
+ )
159
+ except BudgetExceeded as e:
160
+ out.stop_reason, out.error = "budget_exceeded", str(e)
161
+ break
162
+ out.steps += 1
163
+ out.usage.add(usage)
164
+ if not resp.choices:
165
+ out.stop_reason, out.error = "model_error", "empty response"
166
+ break
167
+ msg = resp.choices[0].message
168
+ finish = getattr(resp.choices[0], "finish_reason", None)
169
+ text = message_text(msg.content)
170
+ if not msg.tool_calls and (finish == "length" or not text.strip()):
171
+ # Thinking models can spend the whole reply budget reasoning. Ask once more instead of
172
+ # treating an empty or truncated reply as "done". Same rule for every model.
173
+ if nudges < MAX_NUDGES:
174
+ nudges += 1
175
+ messages.append({"role": "assistant", "content": text.strip() or "(no reply)"})
176
+ messages.append({"role": "user", "content": NUDGE})
177
+ continue
178
+ out.stop_reason = "no_action"
179
+ break
180
+ messages.append(_assistant_dict(msg))
181
+ if not msg.tool_calls:
182
+ out.stop_reason = "finished"
183
+ out.final_message = text
184
+ break
185
+ nudges = 0
186
+ for tc in msg.tool_calls:
187
+ out.tool_calls += 1
188
+ try:
189
+ args = json.loads(tc.function.arguments or "{}")
190
+ command = args["command"] if tc.function.name == "bash" else None
191
+ except (json.JSONDecodeError, KeyError, TypeError):
192
+ command = None
193
+ if not isinstance(command, str):
194
+ result = 'error: call the bash tool with a JSON object {"command": "..."}'
195
+ else:
196
+ if on_step:
197
+ on_step(out.steps, command)
198
+ r = sandbox.exec(command)
199
+ result = truncate(r.output, cfg.max_output_chars) + f"\n[exit code {r.exit_code}]"
200
+ if seen.get(command) == result:
201
+ result += "\n" + REPEAT_NOTE
202
+ else:
203
+ seen[command] = result
204
+ messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
205
+ return out
commitarena/cli.py ADDED
@@ -0,0 +1,313 @@
1
+ """Command line interface: mine, run, report, demo."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import webbrowser
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Annotated
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+
15
+ from . import __version__
16
+ from .errors import CommitArenaError
17
+
18
+ app = typer.Typer(
19
+ add_completion=False,
20
+ no_args_is_help=True,
21
+ rich_markup_mode="rich",
22
+ help="Your git history is the benchmark. Replay real bug fixes against any model, in a sandbox with "
23
+ "no network.",
24
+ )
25
+ # Wide enough for the leaderboard when output is piped (Rich falls back to 80 columns).
26
+ console = Console(width=None if sys.stdout.isatty() else 120, highlight=False)
27
+ err = Console(stderr=True)
28
+
29
+
30
+ def _version(value: bool) -> None:
31
+ if value:
32
+ console.print(f"commitarena {__version__}")
33
+ raise typer.Exit()
34
+
35
+
36
+ @app.callback()
37
+ def _root(
38
+ version: Annotated[
39
+ bool, typer.Option("--version", callback=_version, is_eager=True, help="Show the version.")
40
+ ] = False,
41
+ ) -> None:
42
+ pass
43
+
44
+
45
+ def _parse_pairs(values: list[str] | None, what: str) -> dict[str, str]:
46
+ out: dict[str, str] = {}
47
+ for v in values or []:
48
+ if "=" not in v:
49
+ raise CommitArenaError(f"{what} must look like MODEL=VALUE, got '{v}'.")
50
+ k, val = v.rsplit("=", 1)
51
+ out[k.strip()] = val.strip()
52
+ return out
53
+
54
+
55
+ def _csv(value: str | None) -> list[str]:
56
+ return [x.strip() for x in (value or "").split(",") if x.strip()]
57
+
58
+
59
+ @app.command()
60
+ def mine(
61
+ repo: Annotated[str, typer.Argument(help="Git URL or local path of a Python repo that uses pytest.")],
62
+ out: Annotated[Path | None, typer.Option(help="Where to write tasks.json.")] = None,
63
+ rev: Annotated[str, typer.Option(help="Branch or commit to walk back from.")] = "HEAD",
64
+ since: Annotated[str | None, typer.Option(help="Only commits after this date (git --since).")] = None,
65
+ until: Annotated[str | None, typer.Option(help="Only commits before this date (git --until).")] = None,
66
+ scan: Annotated[int, typer.Option(help="How many recent commits to scan.")] = 200,
67
+ max_tasks: Annotated[int, typer.Option(help="Stop after this many validated tasks.")] = 10,
68
+ python: Annotated[str, typer.Option(help="Python version of the test image.")] = "3.12",
69
+ setup: Annotated[
70
+ str, typer.Option(help="Install command run once at image build time (with network).")
71
+ ] = "pip install --no-cache-dir -e .",
72
+ test_timeout: Annotated[int, typer.Option(help="Seconds allowed for one test-suite run.")] = 600,
73
+ ) -> None:
74
+ """Find commits that fix a bug with a test, and keep those whose new tests fail without the fix."""
75
+ from .mine import mine as do_mine
76
+ from .sandbox import EnvSpec
77
+ from .tasks import repo_slug
78
+
79
+ out = out or Path("tasks") / repo_slug(repo) / "tasks.json"
80
+
81
+ def on_event(kind: str, d: dict) -> None:
82
+ if kind == "build":
83
+ console.print(f"[dim]building test image {d['image']} (network on, only for this step)[/dim]")
84
+ elif kind == "candidates":
85
+ console.print(f"{d['count']} candidate commits touch both source and tests")
86
+ elif kind == "task":
87
+ console.print(f" [green]valid[/green] {d['id']} {d['f2p']} fail->pass, {d['p2p']} pass->pass")
88
+ elif kind == "reject":
89
+ console.print(f" [dim]skip {d['sha']} {d['reason']}[/dim]")
90
+
91
+ tasks, meta = do_mine(
92
+ repo, out=out, rev=rev, since=since, until=until, scan=scan, max_tasks=max_tasks,
93
+ env=EnvSpec(python=python, setup=setup), test_timeout=test_timeout, on_event=on_event,
94
+ ) # fmt: skip
95
+ console.print(f"\n[bold]{len(tasks)} validated tasks[/bold] written to {out}")
96
+ if tasks:
97
+ console.print(f"Next: commitarena run --tasks {out} --models ollama/qwen3:4b")
98
+
99
+
100
+ def results_table(results, cutoffs: dict[str, str] | None = None) -> Table:
101
+ from .report import leaderboard
102
+
103
+ rows = leaderboard([r.to_dict() if hasattr(r, "to_dict") else r for r in results], cutoffs or {})
104
+ t = Table(title="Leaderboard", title_justify="left")
105
+ t.add_column("model", no_wrap=True)
106
+ for col in ("pass", "rate", "before cutoff", "after cutoff", "cost $", "tokens", "avg s", "avg steps"):
107
+ t.add_column(col, justify="right")
108
+
109
+ def split(s):
110
+ return f"{s['passed']}/{s['total']}" if s["total"] else "-"
111
+
112
+ for r in rows:
113
+ t.add_row(
114
+ r["model"], f"{r['passed']}/{r['total']}", f"{r['rate'] * 100:.0f}%", split(r["before"]),
115
+ split(r["after"]), f"{r['cost']:.4f}", f"{r['tokens']:,}", f"{r['avg_time']:.0f}",
116
+ f"{r['avg_steps']:.1f}",
117
+ ) # fmt: skip
118
+ return t
119
+
120
+
121
+ @app.command()
122
+ def run(
123
+ tasks: Annotated[Path, typer.Option(help="tasks.json from `commitarena mine`.")],
124
+ models: Annotated[str, typer.Option(help="Comma-separated provider/model, e.g. ollama/qwen3:4b.")],
125
+ out: Annotated[Path | None, typer.Option(help="Run directory (resumable).")] = None,
126
+ max_cost: Annotated[float, typer.Option(help="Stop before any call once this many USD are spent.")] = 1.0,
127
+ task_ids: Annotated[str | None, typer.Option(help="Comma-separated task ids to run.")] = None,
128
+ limit: Annotated[int | None, typer.Option(help="Run only the first N tasks.")] = None,
129
+ max_steps: Annotated[int, typer.Option(help="Model calls per attempt.")] = 30,
130
+ max_time: Annotated[int, typer.Option(help="Seconds per attempt.")] = 1200,
131
+ context_file: Annotated[
132
+ Path | None, typer.Option(help="AGENTS.md/CLAUDE.md to inject into the attempt.")
133
+ ] = None,
134
+ context_mode: Annotated[str, typer.Option(help="both | repo | prompt")] = "both",
135
+ strip_context_files: Annotated[
136
+ bool, typer.Option(help="Remove the repo's own AGENTS.md / CLAUDE.md first.")
137
+ ] = False,
138
+ cutoff: Annotated[
139
+ list[str] | None, typer.Option(help="Training cutoff, MODEL=YYYY-MM-DD (repeatable).")
140
+ ] = None,
141
+ price: Annotated[
142
+ list[str] | None, typer.Option(help="Price override, MODEL=IN,OUT in USD per 1M tokens (repeatable).")
143
+ ] = None,
144
+ cpus: Annotated[float, typer.Option(help="CPU limit of the sandbox.")] = 2.0,
145
+ memory: Annotated[str, typer.Option(help="Memory limit of the sandbox.")] = "2g",
146
+ command_timeout: Annotated[int, typer.Option(help="Seconds per agent command.")] = 120,
147
+ ) -> None:
148
+ """Run every task against every model with the same minimal agent, then write report.html."""
149
+ from .agent import AgentConfig
150
+ from .llm import Budget, ChatClient, ModelSpec
151
+ from .report import write_report
152
+ from .runner import ContextFile, run_suite
153
+ from .sandbox import SandboxConfig, ensure_docker
154
+ from .tasks import load_tasks
155
+
156
+ prices = {}
157
+ for k, v in _parse_pairs(price, "--price").items():
158
+ try:
159
+ pin, pout = (float(x) for x in v.split(","))
160
+ except ValueError as e:
161
+ raise CommitArenaError(f"--price for {k} must be IN,OUT (numbers), got '{v}'.") from e
162
+ prices[k] = (pin, pout)
163
+ specs = [ModelSpec.parse(m, prices.get(m)) for m in _csv(models)]
164
+ if not specs:
165
+ raise CommitArenaError("--models is empty.")
166
+ for s in specs: # validate keys and prices before any work
167
+ ChatClient(s)._sdk()
168
+ if s.is_local:
169
+ _warn_ollama_context(s.name)
170
+ task_list = load_tasks(tasks, _csv(task_ids) or None)
171
+ if limit:
172
+ task_list = task_list[:limit]
173
+ ensure_docker()
174
+ out = out or Path("runs") / datetime.now().strftime("%Y%m%d-%H%M%S")
175
+ ctx = ContextFile.from_path(context_file, context_mode) if context_file else None
176
+ budget = Budget(max_cost=max_cost)
177
+ cutoffs = _parse_pairs(cutoff, "--cutoff")
178
+ console.print(
179
+ f"{len(task_list)} tasks x {len(specs)} models, sandbox --network none, {cpus} CPU, {memory}, "
180
+ f"budget ${max_cost:.2f}. Results in {out}"
181
+ )
182
+
183
+ def on_start(task, model):
184
+ console.print(f"[bold]{model.id}[/bold] {task.id} [dim]({task.date[:10]})[/dim]")
185
+
186
+ def on_step(step, command):
187
+ first = command.strip().splitlines()[0] if command.strip() else ""
188
+ console.print(f" [dim]{step:>2} $ {first[:100]}[/dim]")
189
+
190
+ def on_result(r):
191
+ mark = "[green]PASS[/green]" if r.passed else "[red]FAIL[/red]"
192
+ console.print(
193
+ f" {mark} {r.fail_to_pass_passed}/{r.fail_to_pass_total} fail->pass, "
194
+ f"{r.pass_to_pass_failed} broken, {r.steps} steps, {r.wall_time_s:.0f}s, "
195
+ f"${r.cost_usd:.4f} ({r.stop_reason}) total ${budget.spent:.4f}"
196
+ )
197
+
198
+ results, abort = run_suite(
199
+ task_list, specs, out, budget=budget, context=ctx, strip_context_files=strip_context_files,
200
+ agent_config=AgentConfig(max_steps=max_steps, max_wall_time=max_time),
201
+ sandbox_config=SandboxConfig(cpus=cpus, memory=memory, command_timeout=command_timeout),
202
+ cutoffs=cutoffs, on_result=on_result, on_start=on_start, on_step=on_step,
203
+ ) # fmt: skip
204
+ report = write_report(out)
205
+ console.print(results_table(results, _load_cutoffs(out)))
206
+ console.print(f"Spent ${budget.spent:.4f}. Report: {report}")
207
+ if abort:
208
+ err.print(f"[red]{abort}[/red]")
209
+ raise typer.Exit(2)
210
+
211
+
212
+ def _warn_ollama_context(name: str) -> None:
213
+ """Ollama's OpenAI endpoint ignores num_ctx; a small default window silently drops the task text."""
214
+ import json
215
+ import os
216
+ import urllib.request
217
+
218
+ from .llm import _ollama_base
219
+
220
+ if os.environ.get("OLLAMA_CONTEXT_LENGTH"):
221
+ return
222
+ try:
223
+ req = urllib.request.Request(
224
+ _ollama_base().removesuffix("/v1") + "/api/show", data=json.dumps({"model": name}).encode()
225
+ )
226
+ with urllib.request.urlopen(req, timeout=10) as r:
227
+ info = json.load(r)
228
+ except Exception: # noqa: BLE001 - the real call reports connection problems
229
+ return
230
+ if "num_ctx" not in (info.get("parameters") or ""):
231
+ err.print(
232
+ f"[yellow]warning:[/yellow] ollama/{name} has no num_ctx set, so Ollama may serve it with a small "
233
+ f"default context (often 4096 tokens) and drop the task text. Create a variant:\n"
234
+ f" printf 'FROM {name}\\nPARAMETER num_ctx 16384\\n' > Modelfile && "
235
+ f"ollama create {name.split(':')[0]}:16k -f Modelfile"
236
+ )
237
+
238
+
239
+ def _load_cutoffs(run_dir: Path) -> dict[str, str]:
240
+ import json
241
+
242
+ from .report import DEFAULT_CUTOFFS
243
+
244
+ meta = json.loads((run_dir / "run.json").read_text()) if (run_dir / "run.json").exists() else {}
245
+ return {**DEFAULT_CUTOFFS, **meta.get("cutoffs", {})}
246
+
247
+
248
+ @app.command()
249
+ def report(
250
+ run_dir: Annotated[Path, typer.Argument(help="Run directory written by `commitarena run`.")],
251
+ out: Annotated[Path | None, typer.Option(help="Output HTML (default: <run_dir>/report.html).")] = None,
252
+ cutoff: Annotated[
253
+ list[str] | None, typer.Option(help="Override a training cutoff, MODEL=YYYY-MM-DD.")
254
+ ] = None,
255
+ open_browser: Annotated[bool, typer.Option("--open", help="Open the report in a browser.")] = False,
256
+ ) -> None:
257
+ """Render the static HTML report: leaderboard, task x model matrix, cutoff split, diffs."""
258
+ from .report import load_run, write_report
259
+
260
+ if not (run_dir / "results.jsonl").exists():
261
+ raise CommitArenaError(f"No results.jsonl in {run_dir}. Run `commitarena run` first.")
262
+ path = write_report(run_dir, out, _parse_pairs(cutoff, "--cutoff"))
263
+ data = load_run(run_dir)
264
+ console.print(
265
+ results_table(data["results"], {**_load_cutoffs(run_dir), **_parse_pairs(cutoff, "--cutoff")})
266
+ )
267
+ console.print(f"Report: {path}")
268
+ if open_browser:
269
+ webbrowser.open(path.resolve().as_uri())
270
+
271
+
272
+ @app.command()
273
+ def demo(
274
+ out: Annotated[Path, typer.Option(help="Where to write the demo report.")] = Path(
275
+ "commitarena-demo.html"
276
+ ),
277
+ open_browser: Annotated[bool, typer.Option("--open/--no-open", help="Open it in a browser.")] = True,
278
+ ) -> None:
279
+ """Show a RECORDED real run (no keys, no Docker): leaderboard in the terminal plus the HTML report."""
280
+ from .report import demo_dir, load_run, write_report
281
+
282
+ d = demo_dir()
283
+ data = load_run(d)
284
+ m = data["meta"]
285
+ console.print(
286
+ f"[bold]Recorded result[/bold] (not a live run): {m.get('updated_at', '')[:10]}, commitarena "
287
+ f"{m.get('version')}, models {', '.join(m.get('models', []))}, on {m.get('hardware', '?')}."
288
+ )
289
+ console.print(f"Repo: {m.get('repo', '')} ({m.get('repo_license', '')})")
290
+ console.print(results_table(data["results"], data["cutoffs"]))
291
+ path = write_report(d, out)
292
+ console.print(f"Report: {path}")
293
+ console.print("Run it yourself: commitarena mine <repo> && commitarena run --tasks ... --models ...")
294
+ if open_browser:
295
+ webbrowser.open(path.resolve().as_uri())
296
+
297
+
298
+ def _sigterm(signum, frame): # noqa: ARG001
299
+ raise KeyboardInterrupt
300
+
301
+
302
+ def main() -> None:
303
+ import signal
304
+
305
+ signal.signal(signal.SIGTERM, _sigterm) # so sandboxes are removed on kill
306
+ try:
307
+ app()
308
+ except CommitArenaError as e:
309
+ err.print(f"[red]error:[/red] {e}")
310
+ sys.exit(1)
311
+ except KeyboardInterrupt:
312
+ err.print("[yellow]interrupted[/yellow]")
313
+ sys.exit(130)
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2016, Andi Albrecht <albrecht.andi@gmail.com>
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name of the authors nor the names of its contributors may be
13
+ used to endorse or promote products derived from this software without
14
+ specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.