Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:28:25.219Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

cli.py

788 lines · 26.5 KB · python
1from __future__ import annotations
2
3import argparse
4import json
5import os
6import subprocess
7import sys
8from pathlib import Path
9
10from . import benchmarks, config, source
11from .baseline import (
12    command_baseline_list,
13    command_baseline_record,
14    command_baseline_show,
15)
16from .common import (
17    AppNotDeployedError,
18    command_exists,
19    default_namespace,
20    deploy_app,
21    deployed_function,
22    print_json,
23)
24from .launch import command_build, command_rejudge, command_run, command_swe_atlas
25from .report import command_report
26from .volume import (
27    command_builds,
28    command_fetch,
29    command_list,
30    command_logs,
31    command_runs,
32    command_status,
33    command_suite_fetch,
34    command_suite_logs,
35    command_suite_status,
36    ensure_volume_exists,
37)
38
39
40def modal_secret_names() -> set[str]:
41    result = subprocess.run(
42        ["modal", "secret", "list", "--json"],
43        check=True,
44        capture_output=True,
45        text=True,
46    )
47    data = json.loads(result.stdout)
48    if not isinstance(data, list):
49        return set()
50    return {
51        item["name"] for item in data if isinstance(item, dict) and item.get("name")
52    }
53
54
55def command_doctor(args: argparse.Namespace) -> int:
56    namespace = default_namespace(args)
57    print("zed-eval doctor")
58    print(f"  namespace:          {namespace}")
59    print(f"  app:                {args.app_name}")
60    print(f"  volume:             {args.volume}")
61    print(f"  api secret:         {args.api_secret}")
62    print(f"  modal token secret: {args.modal_token_secret}")
63    print(f"  repo root:          {source.repo_root()}")
64    print(f"  base sha:           {source.current_base_sha()}")
65    print(f"  default model:      {config.DEFAULT_MODEL}")
66    print("  default judges:     qna=deepseek-v4-pro, rf/tw=kimi-k2.7-code")
67
68    missing = []
69    for executable in ("git", "modal", "harbor"):
70        if command_exists(executable):
71            print(f"  {executable}:             found")
72        else:
73            print(f"  {executable}:             missing")
74            missing.append(executable)
75
76    if missing:
77        print("\nMissing required executables: " + ", ".join(missing), file=sys.stderr)
78        return 1
79
80    try:
81        secrets = modal_secret_names()
82    except (subprocess.CalledProcessError, json.JSONDecodeError) as error:
83        print(
84            f"  secrets:          could not list Modal secrets: {error}",
85            file=sys.stderr,
86        )
87    else:
88        missing_secrets = [
89            secret_name
90            for secret_name in (args.api_secret, args.modal_token_secret)
91            if secret_name not in secrets
92        ]
93        if missing_secrets:
94            print(
95                "\nMissing Modal secret(s): " + ", ".join(missing_secrets),
96                file=sys.stderr,
97            )
98            print(
99                "Use --api-secret/--modal-token-secret to point at existing secrets, "
100                "or create the missing ones with `modal secret create`.",
101                file=sys.stderr,
102            )
103            return 1
104        print("  secrets:          found")
105        print(
106            "  controller token: use a dedicated Modal service-user token for production"
107        )
108
109    if args.create_volume:
110        return ensure_volume_exists(args)
111    return 0
112
113
114def command_deploy(args: argparse.Namespace) -> int:
115    deploy_app(args)
116    print(f"Deployed Modal app '{args.app_name}'.")
117    return 0
118
119
120def command_cleanup(args: argparse.Namespace) -> int:
121    request: dict[str, object] = {"dry_run": bool(args.dry_run)}
122    if args.build_retention_days is not None:
123        request["build_retention_days"] = args.build_retention_days
124    result = deployed_function(args, "cleanup_artifacts").remote(request)
125    print_json(result)
126    return 0
127
128
129def env_default(
130    variable_name: str, default: object, *, include_defaults: bool = True
131) -> object:
132    if not include_defaults:
133        return argparse.SUPPRESS
134    return os.environ.get(variable_name, default)
135
136
137def add_common_options(
138    parser: argparse.ArgumentParser, *, include_defaults: bool = True
139) -> None:
140    default_volume = env_default(
141        "AGENT_EVALS_VOLUME",
142        config.DEFAULT_VOLUME_NAME,
143        include_defaults=include_defaults,
144    )
145    default_namespace = None if include_defaults else argparse.SUPPRESS
146    default_api_secret = env_default(
147        "AGENT_EVALS_LLM_PROVIDERS_SECRET",
148        config.DEFAULT_LLM_PROVIDERS_SECRET_NAME,
149        include_defaults=include_defaults,
150    )
151    default_modal_token_secret = env_default(
152        "AGENT_EVALS_MODAL_TOKEN_SECRET",
153        config.DEFAULT_MODAL_TOKEN_SECRET_NAME,
154        include_defaults=include_defaults,
155    )
156    default_app_name = env_default(
157        "AGENT_EVALS_APP_NAME",
158        config.DEFAULT_APP_NAME,
159        include_defaults=include_defaults,
160    )
161    infra = parser.add_argument_group(
162        "infra options", "Modal app/volume/namespace and secret names (rarely changed)"
163    )
164    infra.add_argument(
165        "--app-name",
166        default=default_app_name,
167        help=f"Deployed Modal app name (default: {config.DEFAULT_APP_NAME})",
168    )
169    infra.add_argument(
170        "--volume",
171        default=default_volume,
172        help=f"Modal volume for builds/runs (default: {config.DEFAULT_VOLUME_NAME})",
173    )
174    infra.add_argument(
175        "--namespace",
176        default=default_namespace,
177        help="Run namespace inside the shared volume (default: env/git user/local user)",
178    )
179    infra.add_argument(
180        "--api-secret",
181        default=default_api_secret,
182        help=(
183            "Modal secret with the LLM provider keys mounted into trial sandboxes "
184            f"(default: {config.DEFAULT_LLM_PROVIDERS_SECRET_NAME})"
185        ),
186    )
187    infra.add_argument(
188        "--modal-token-secret",
189        default=default_modal_token_secret,
190        help=(
191            "Modal secret mounted into the controller with MODAL_TOKEN_ID and MODAL_TOKEN_SECRET; "
192            f"use a dedicated service-user token for production (default: {config.DEFAULT_MODAL_TOKEN_SECRET_NAME})"
193        ),
194    )
195
196
197def add_build_source_options(parser: argparse.ArgumentParser) -> None:
198    parser.add_argument(
199        "--from",
200        dest="from_source",
201        default=os.environ.get("AGENT_EVALS_FROM"),
202        help=(
203            "Unified build source: 'local' for current HEAD + tracked changes, "
204            "'main' for origin/main's tip, or a git ref/tag/SHA (e.g. v0.210.0) "
205            "for a clean build resolved canonically against the remote so "
206            "teammates share one build"
207        ),
208    )
209    parser.add_argument("--base-sha")
210    parser.add_argument("--patch-path")
211    parser.add_argument(
212        "--repo-url",
213        default=os.environ.get("AGENT_EVALS_REPO_URL", source.DEFAULT_REPO_URL),
214        help="Git remote the Modal builder fetches the base SHA from",
215    )
216    parser.add_argument(
217        "--allow-untracked",
218        action="store_true",
219        help="Proceed even when untracked files exist; they are not included in the build patch",
220    )
221    parser.add_argument(
222        "--require-clean",
223        action="store_true",
224        help="Fail if tracked changes are present",
225    )
226    parser.add_argument(
227        "--clean-source",
228        action="store_true",
229        help="Build exactly --base-sha/--zed-version with no local patch",
230    )
231    parser.add_argument(
232        "--zed-version",
233        help="Git ref/tag/SHA of Zed to build as a clean source snapshot",
234    )
235
236
237def add_model_options(parser: argparse.ArgumentParser) -> None:
238    parser.add_argument(
239        "-m",
240        "--model",
241        default=os.environ.get("AGENT_EVALS_MODEL", config.DEFAULT_MODEL),
242        help="Base model as provider/model, sonnet-4.6, baseten:kimi-k2.7-code, or baseten:<model-id>",
243    )
244    parser.add_argument(
245        "--extra-api-secret",
246        action="append",
247        help="Additional Modal secret name to mount into the trial sandbox",
248    )
249    advanced = parser.add_argument_group(
250        "advanced model options",
251        "Lower-level model routing. For Baseten, prefer --model baseten:<model-id>.",
252    )
253    advanced.add_argument(
254        "--model-provider",
255        choices=("zed", "baseten"),
256        default="zed",
257        help="Use 'zed' for built-in provider/model ids, or 'baseten' for Baseten Model APIs",
258    )
259    advanced.add_argument(
260        "--baseten-model",
261        help="Baseten model id when --model-provider baseten, e.g. moonshotai/Kimi-K2.7-Code",
262    )
263    advanced.add_argument("--baseten-model-display-name")
264    advanced.add_argument(
265        "--baseten-api-url",
266        default=config.BASETEN_API_URL,
267        help=f"Baseten OpenAI-compatible API URL (default: {config.BASETEN_API_URL})",
268    )
269    advanced.add_argument(
270        "--baseten-model-max-tokens",
271        type=int,
272        default=config.BASETEN_DEFAULT_MAX_TOKENS,
273    )
274    advanced.add_argument(
275        "--baseten-model-max-output-tokens",
276        type=int,
277        default=config.BASETEN_DEFAULT_MAX_OUTPUT_TOKENS,
278    )
279    advanced.add_argument(
280        "--openai-compatible-provider-json",
281        help="JSON object merged into language_models.openai_compatible before eval-cli resolves --model",
282    )
283    advanced.add_argument(
284        "--anthropic-available-models-json",
285        help="JSON array merged into language_models.anthropic.available_models before eval-cli resolves --model",
286    )
287
288
289def add_launch_options(parser: argparse.ArgumentParser) -> None:
290    add_model_options(parser)
291    parser.add_argument(
292        "-j",
293        "--judge",
294        default=os.environ.get("AGENT_EVALS_JUDGE", config.DEFAULT_JUDGE_PRESET),
295        choices=["auto", *sorted(config.JUDGES)],
296        help="Judge preset; auto uses qna=deepseek-v4-pro and rf/tw=kimi-k2.7-code",
297    )
298    parser.add_argument("--judge-model")
299    parser.add_argument(
300        "--build",
301        metavar="ID",
302        help="Reuse this build if it exists, otherwise create it with this id",
303    )
304    add_build_source_options(parser)
305    parser.add_argument("--build-wait-timeout-secs", type=int, default=7200)
306    parser.add_argument("--tasks", help="File containing one full task name per line")
307    parser.add_argument("--include-task-name", action="append")
308    parser.add_argument("-n", "--n-tasks", type=int, help="Forward harness --n-tasks")
309    parser.add_argument("--n-concurrent", type=int, default=config.DEFAULT_N_CONCURRENT)
310    parser.add_argument(
311        "--override-cpus", type=int, default=config.DEFAULT_OVERRIDE_CPUS
312    )
313    parser.add_argument(
314        "--override-memory-mb", type=int, default=config.DEFAULT_OVERRIDE_MEMORY_MB
315    )
316    parser.add_argument(
317        "--sandbox-timeout-secs", type=int, default=config.DEFAULT_SANDBOX_TIMEOUT_SECS
318    )
319    parser.add_argument(
320        "--sandbox-idle-timeout-secs",
321        type=int,
322        default=config.DEFAULT_SANDBOX_IDLE_TIMEOUT_SECS,
323    )
324    parser.add_argument(
325        "--eval-cli-timeout",
326        type=int,
327        help="Override the per-task agent timeout (defaults to the benchmark's)",
328    )
329    parser.add_argument("--run-id")
330    parser.add_argument(
331        "--dry-run",
332        action="store_true",
333        help="Print full manifests and harness commands without launching",
334    )
335    parser.add_argument(
336        "--plan",
337        action="store_true",
338        help="Print a concise launch plan without launching",
339    )
340    parser.add_argument(
341        "--verbose",
342        action="store_true",
343        help="With --plan, include full manifests and harness commands",
344    )
345    parser.add_argument(
346        "--extra-harbor-arg",
347        action="append",
348        help="Append one raw argument to the harness run command",
349    )
350    parser.add_argument(
351        "--swe-atlas-repo-url",
352        default=os.environ.get(
353            "AGENT_EVALS_SWE_ATLAS_REPO_URL", benchmarks.SWE_ATLAS_REPO_URL
354        ),
355        help="SWE-Atlas repo URL used for path-backed datasets",
356    )
357    parser.add_argument(
358        "--swe-atlas-repo-ref",
359        default=os.environ.get(
360            "AGENT_EVALS_SWE_ATLAS_REPO_REF", benchmarks.SWE_ATLAS_REPO_REF
361        ),
362        help="SWE-Atlas repo ref used for path-backed datasets",
363    )
364
365
366def add_suite_options(parser: argparse.ArgumentParser) -> None:
367    add_launch_options(parser)
368    parser.add_argument(
369        "--parts",
370        help="Comma-separated SWE-Atlas parts to run: qna,rf,tw (or 'all')",
371    )
372    parser.add_argument("--experiment-prefix", default="swe-atlas")
373    parser.add_argument("--run-id-prefix")
374    parser.add_argument(
375        "--suite-id", help="Explicit suite id for grouping multi-part runs"
376    )
377    parser.add_argument("--interactive", action="store_true")
378    parser.add_argument(
379        "-y",
380        "--yes",
381        action="store_true",
382        help="Never prompt; require flags/defaults",
383    )
384
385
386def add_run_lookup_options(
387    parser: argparse.ArgumentParser,
388    *,
389    require_experiment: bool = False,
390    run_id_optional: bool = False,
391) -> None:
392    if run_id_optional:
393        parser.add_argument(
394            "run_id",
395            nargs="?",
396            help="Run id (defaults to the most recent run launched from this machine)",
397        )
398    else:
399        parser.add_argument("run_id")
400    parser.add_argument(
401        "-e",
402        "--experiment-name",
403        required=require_experiment,
404        help="Benchmark storage name (auto-resolved from the local run index when omitted)",
405    )
406
407
408def add_command_parser(
409    subparsers,
410    name: str,
411    *,
412    func: object | None = None,
413    **kwargs,
414) -> argparse.ArgumentParser:
415    parser = subparsers.add_parser(name, **kwargs)
416    add_common_options(parser, include_defaults=False)
417    if func is not None:
418        parser.set_defaults(func=func)
419    return parser
420
421
422def build_parser() -> argparse.ArgumentParser:
423    parser = argparse.ArgumentParser(
424        prog="zed-eval",
425        description="Launch, monitor, and fetch remote SWE-Atlas agent evals on Modal.",
426        formatter_class=argparse.RawDescriptionHelpFormatter,
427        epilog=(
428            "Common workflows:\n"
429            "  zed-eval run swe-atlas -m sonnet-4.6       # launch the SWE-Atlas suite\n"
430            "  zed-eval run rf -m sonnet-4.6              # launch one benchmark\n"
431            "  zed-eval runs                              # what did I launch recently?\n"
432            "  zed-eval status                            # check my most recent run\n"
433            "  zed-eval status <run-id>                   # check a specific run\n"
434            "  zed-eval logs <run-id>                     # print controller logs\n"
435            "  zed-eval report <run-id> --fetch           # fetch + score a run\n"
436            "\n"
437            "After launching, a run id alone locates the run — namespace and\n"
438            "benchmark storage name are resolved from this machine's local run index."
439        ),
440    )
441    add_common_options(parser)
442    subparsers = parser.add_subparsers(dest="command", required=True)
443
444    doctor = add_command_parser(
445        subparsers,
446        "doctor",
447        func=command_doctor,
448        help="Check local prerequisites and defaults",
449    )
450    doctor.add_argument("--create-volume", action="store_true")
451
452    add_command_parser(
453        subparsers,
454        "deploy",
455        func=command_deploy,
456        help=(
457            "Publish harness code to Modal. This is the ONLY command that "
458            "deploys; deploying cancels in-flight runs. Run it once after "
459            "changing harness code — all other commands just invoke the "
460            "already-deployed functions and never deploy."
461        ),
462    )
463
464    build = add_command_parser(
465        subparsers,
466        "build",
467        func=command_build,
468        help="Build eval-cli on Modal into builds/<build-id>",
469    )
470    add_build_source_options(build)
471    build.add_argument(
472        "--build",
473        metavar="ID",
474        help="Reuse this build if it exists, otherwise create it with this id",
475    )
476    build.add_argument(
477        "--detach", action="store_true", help="Spawn the build and return immediately"
478    )
479
480    builds = add_command_parser(
481        subparsers,
482        "builds",
483        func=command_builds,
484        help="List content-addressed builds on the volume",
485    )
486    builds.add_argument("--details", action="store_true")
487    builds.add_argument("--json", action="store_true")
488    builds.add_argument("--limit", type=int, default=50)
489
490    run_cmd = add_command_parser(
491        subparsers,
492        "run",
493        func=command_run,
494        help="Run benchmarks — the everyday entry point",
495        description=(
496            "Launch runs by benchmark id, alias, or group. It picks the right "
497            "default judge for each benchmark and never prompts. The sibling "
498            "`swe-atlas` command launches SWE-Atlas parts (qna/rf/tw) with "
499            "interactive prompts."
500        ),
501    )
502    run_cmd.add_argument(
503        "benchmark",
504        nargs="+",
505        metavar="target",
506        help=(
507            "Benchmark id/alias/group (swe-atlas (= qna,rf,tw), swe-atlas-rf, "
508            "qna, rf, tw, terminal-bench-2.1 (tb21), deepswe). Comma-separated "
509            "and repeated values are combined."
510        ),
511    )
512    add_launch_options(run_cmd)
513
514    run_cmd.add_argument(
515        "--suite-id", help="Explicit suite id for multi-benchmark runs"
516    )
517    run_cmd.add_argument(
518        "--staff",
519        action="store_true",
520        help=(
521            "Run the agent with staff mode ON (default OFF). Staff mode enables "
522            "the sandboxed terminal, which hangs inside Modal sandboxes, so keep "
523            "it off for remote runs."
524        ),
525    )
526    run_cmd.add_argument(
527        "-y",
528        "--yes",
529        action="store_true",
530        help="Accepted for parity with swe-atlas; run never prompts",
531    )
532
533    report = add_command_parser(
534        subparsers,
535        "report",
536        func=command_report,
537        help="Success-conditioned metrics (rate, tokens, tool calls, steps)",
538    )
539    report.add_argument("run_id", nargs="?")
540    report.add_argument(
541        "-e",
542        "--experiment-name",
543        help="Benchmark storage name (auto-resolved from the local run index when omitted)",
544    )
545    report.add_argument("--job-dir", help="Analyze a local job directory directly")
546    report.add_argument("--jobs-dir", default=str(Path.home() / ".cache/harbor/jobs"))
547    report.add_argument(
548        "--fetch", action="store_true", help="Fetch the run archive before reporting"
549    )
550    report.add_argument(
551        "--json",
552        dest="as_json",
553        action="store_true",
554        help="Emit JSON instead of a table",
555    )
556    report.add_argument(
557        "--timeouts-as-failures",
558        dest="timeouts_as_failures",
559        action=argparse.BooleanOptionalAction,
560        default=None,
561        help=(
562            "Count agent timeouts as failures rather than excluded errors "
563            "(default: auto — on for test-scored benchmarks like terminal-bench "
564            "and deepswe)"
565        ),
566    )
567
568    cleanup = add_command_parser(
569        subparsers,
570        "cleanup",
571        func=command_cleanup,
572        help="Prune stale builds and cold build cache (never eval results)",
573    )
574    cleanup.add_argument(
575        "--dry-run",
576        action="store_true",
577        help="Report what would be removed without deleting anything",
578    )
579    cleanup.add_argument(
580        "--build-retention-days",
581        type=float,
582        help="Remove builds older than this many days (default 14)",
583    )
584
585    suite = add_command_parser(
586        subparsers,
587        "swe-atlas",
588        func=command_swe_atlas,
589        help="Launch an interactive benchmark suite (SWE-Atlas, Terminal-Bench, DeepSWE)",
590    )
591    add_suite_options(suite)
592
593    list_runs = add_command_parser(
594        subparsers,
595        "list",
596        func=command_list,
597        help="List benchmark runs on the volume, optionally with metadata",
598    )
599    list_runs.add_argument("-e", "--experiment-name")
600    list_runs.add_argument("--details", action="store_true")
601    list_runs.add_argument("--json", action="store_true")
602    list_runs.add_argument("--limit", type=int, default=50)
603    list_runs.add_argument("--all-namespaces", action="store_true")
604
605    runs = add_command_parser(
606        subparsers,
607        "runs",
608        func=command_runs,
609        help="List recent runs launched from this machine (local, fast)",
610        description=(
611            "Show runs recorded in the local run index when you launched them "
612            "from this machine — newest first, no network call. Use this for a "
613            "quick 'what did I launch lately?'; use `list --details` to query "
614            "run state on the volume, or `status <run-id>` for one run's state."
615        ),
616    )
617    runs.add_argument("--json", action="store_true")
618    runs.add_argument("--limit", type=int, default=20)
619
620    status = add_command_parser(
621        subparsers,
622        "status",
623        func=command_status,
624        help="Print a run's state.json once (no run id = most recent run)",
625    )
626    add_run_lookup_options(status, run_id_optional=True)
627
628    logs = add_command_parser(
629        subparsers,
630        "logs",
631        func=command_logs,
632        help="Print a run's controller.log once (no run id = most recent run)",
633    )
634    add_run_lookup_options(logs, run_id_optional=True)
635
636    fetch = add_command_parser(
637        subparsers,
638        "fetch",
639        func=command_fetch,
640        help="Fetch and extract a run's Harbor job archive",
641    )
642    add_run_lookup_options(fetch)
643    fetch.add_argument("--jobs-dir", default=str(Path.home() / ".cache/harbor/jobs"))
644
645    rejudge = add_command_parser(
646        subparsers,
647        "rejudge",
648        func=command_rejudge,
649        help="Re-grade a finished run with a different judge (new derived run, "
650        "reuses the agent work)",
651    )
652    add_run_lookup_options(rejudge)
653    rejudge.add_argument(
654        "-j",
655        "--judge",
656        required=True,
657        help="Judge preset to re-grade with (see config.JUDGES, e.g. "
658        "deepseek-v4-pro, kimi-k2.7-code, leaderboard)",
659    )
660    rejudge.add_argument(
661        "--judge-model",
662        dest="judge_model",
663        help="Override the judge preset's model id",
664    )
665    rejudge.add_argument(
666        "--parent-namespace",
667        dest="parent_namespace",
668        help="Namespace of the source run, if different from --namespace",
669    )
670    rejudge.add_argument(
671        "--new-run-id",
672        dest="new_run_id",
673        help="Explicit id for the derived run (default: <parent>-rejudge-<judge>-<rand>)",
674    )
675    rejudge.add_argument(
676        "--dry-run",
677        action="store_true",
678        help="Print the rejudge request without spawning the controller",
679    )
680
681    suite_group = add_command_parser(
682        subparsers,
683        "suite",
684        help="Inspect or fetch grouped benchmark suite runs",
685    )
686    suite_subparsers = suite_group.add_subparsers(dest="suite_command", required=True)
687    suite_status = add_command_parser(
688        suite_subparsers,
689        "status",
690        func=command_suite_status,
691        help="Show status for each run in a suite",
692    )
693    suite_status.add_argument("suite_id")
694    suite_status.add_argument("--json", action="store_true")
695    suite_logs = add_command_parser(
696        suite_subparsers,
697        "logs",
698        func=command_suite_logs,
699        help="Print logs for each run in a suite",
700    )
701    suite_logs.add_argument("suite_id")
702    suite_logs.add_argument("--follow", action="store_true")
703    suite_logs.add_argument("--interval", type=float, default=30.0)
704    suite_fetch = add_command_parser(
705        suite_subparsers,
706        "fetch",
707        func=command_suite_fetch,
708        help="Fetch all run archives in a suite",
709    )
710    suite_fetch.add_argument("suite_id")
711    suite_fetch.add_argument(
712        "--jobs-dir", default=str(Path.home() / ".cache/harbor/jobs")
713    )
714
715    baseline_group = add_command_parser(
716        subparsers,
717        "baseline",
718        help="Record and inspect baseline-of-record results (clean commits on main)",
719    )
720    baseline_subparsers = baseline_group.add_subparsers(
721        dest="baseline_command", required=True
722    )
723    baseline_record = add_command_parser(
724        baseline_subparsers,
725        "record",
726        func=command_baseline_record,
727        help="Promote completed run(s) to the baseline of record for their (benchmark, model)",
728    )
729    baseline_record.add_argument("run_id", nargs="+")
730    baseline_record.add_argument("--experiment-name", required=True)
731    baseline_record.add_argument(
732        "--allow-dirty",
733        action="store_true",
734        help="Record even though the build carries a local patch (not a clean commit)",
735    )
736    baseline_record.add_argument(
737        "--allow-off-main",
738        action="store_true",
739        help="Record even though base_sha can't be verified as reachable from origin/main",
740    )
741    baseline_record.add_argument(
742        "--repo-url",
743        default=os.environ.get("AGENT_EVALS_REPO_URL"),
744        help="Git remote to resolve origin/main against (default: AGENT_EVALS_REPO_URL or the canonical repo)",
745    )
746
747    baseline_list = add_command_parser(
748        baseline_subparsers,
749        "list",
750        func=command_baseline_list,
751        help="List the current baseline of record for every (benchmark, model)",
752    )
753    baseline_list.add_argument("--json", action="store_true")
754
755    baseline_show = add_command_parser(
756        baseline_subparsers,
757        "show",
758        func=command_baseline_show,
759        help="Show the baseline record for one (benchmark, model)",
760    )
761    baseline_show.add_argument("experiment_name")
762    baseline_show.add_argument("--model", required=True)
763    baseline_show.add_argument(
764        "--history", action="store_true", help="Include superseded baselines"
765    )
766
767    return parser
768
769
770def main(argv: list[str] | None = None) -> int:
771    parser = build_parser()
772    args = parser.parse_args(argv)
773    try:
774        return args.func(args)
775    except (ValueError, AppNotDeployedError) as error:
776        print(f"error: {error}", file=sys.stderr)
777        return 1
778    except subprocess.CalledProcessError as error:
779        if error.stdout:
780            print(error.stdout, end="")
781        if error.stderr:
782            print(error.stderr, end="", file=sys.stderr)
783        return error.returncode
784
785
786if __name__ == "__main__":
787    raise SystemExit(main())
788
Served at tenant.openagents/omega Member data and write actions are omitted.