evalctl 0.3.0__tar.gz → 0.4.2__tar.gz

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 (31) hide show
  1. {evalctl-0.3.0 → evalctl-0.4.2}/PKG-INFO +54 -15
  2. {evalctl-0.3.0 → evalctl-0.4.2}/README.md +51 -14
  3. {evalctl-0.3.0 → evalctl-0.4.2}/evalctl/__init__.py +1 -1
  4. evalctl-0.4.2/evalctl/artifacts.py +128 -0
  5. evalctl-0.4.2/evalctl/cli.py +36 -0
  6. evalctl-0.4.2/evalctl/commands.py +1062 -0
  7. evalctl-0.4.2/evalctl/doctor.py +193 -0
  8. evalctl-0.4.2/evalctl/inferctl.py +193 -0
  9. evalctl-0.4.2/evalctl/integration_contracts.py +21 -0
  10. evalctl-0.4.2/evalctl/processes.py +86 -0
  11. evalctl-0.4.2/evalctl/reports.py +57 -0
  12. evalctl-0.4.2/evalctl/run_state.py +332 -0
  13. evalctl-0.4.2/evalctl/runner.py +373 -0
  14. evalctl-0.4.2/evalctl/scoring.py +281 -0
  15. evalctl-0.4.2/evalctl/spoolctl.py +208 -0
  16. evalctl-0.4.2/evalctl/static_contract.py +626 -0
  17. evalctl-0.4.2/evalctl/suite.py +226 -0
  18. {evalctl-0.3.0 → evalctl-0.4.2}/evalctl.egg-info/PKG-INFO +54 -15
  19. evalctl-0.4.2/evalctl.egg-info/SOURCES.txt +27 -0
  20. evalctl-0.4.2/evalctl.egg-info/requires.txt +3 -0
  21. {evalctl-0.3.0 → evalctl-0.4.2}/pyproject.toml +14 -1
  22. {evalctl-0.3.0 → evalctl-0.4.2}/tests/test_evalctl.py +1090 -155
  23. evalctl-0.4.2/tests/test_real_spoolctl.py +326 -0
  24. evalctl-0.3.0/evalctl/cli.py +0 -2352
  25. evalctl-0.3.0/evalctl.egg-info/SOURCES.txt +0 -12
  26. {evalctl-0.3.0 → evalctl-0.4.2}/LICENSE +0 -0
  27. {evalctl-0.3.0 → evalctl-0.4.2}/evalctl/__main__.py +0 -0
  28. {evalctl-0.3.0 → evalctl-0.4.2}/evalctl.egg-info/dependency_links.txt +0 -0
  29. {evalctl-0.3.0 → evalctl-0.4.2}/evalctl.egg-info/entry_points.txt +0 -0
  30. {evalctl-0.3.0 → evalctl-0.4.2}/evalctl.egg-info/top_level.txt +0 -0
  31. {evalctl-0.3.0 → evalctl-0.4.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evalctl
3
- Version: 0.3.0
3
+ Version: 0.4.2
4
4
  Summary: Local-first evals for agents, not just prompts.
5
5
  Author: evalctl contributors
6
6
  License: Apache License
@@ -217,6 +217,8 @@ Classifier: Programming Language :: Python :: 3.13
217
217
  Requires-Python: >=3.11
218
218
  Description-Content-Type: text/markdown
219
219
  License-File: LICENSE
220
+ Provides-Extra: dev
221
+ Requires-Dist: coverage>=7; extra == "dev"
220
222
  Dynamic: license-file
221
223
 
222
224
  # evalctl
@@ -232,11 +234,13 @@ durable artifacts. It scores what agents actually do — files written, diffs
232
234
  produced, commands run — on your own machine, with no gateway, dashboard, or
233
235
  SaaS account.
234
236
 
235
- v0.3 writes durable run metadata, supports crash resume, adds local run-state
236
- inspection, and can optionally delegate runner execution to
237
+ v0.4 writes durable run metadata, supports crash resume, adds local run-state
238
+ inspection, diagnoses runtime health, produces side-effect-free execution plans,
239
+ and can optionally delegate runner execution to
237
240
  [spoolctl](https://github.com/Ozhiaki/spoolctl). The standalone synchronous path
238
241
  remains complete and requires no external service. [inferctl](https://inferctl.dev)
239
- route capture remains deferred.
242
+ preflight provenance can be captured before runner execution without changing
243
+ report scoring.
240
244
 
241
245
  ## About
242
246
 
@@ -259,17 +263,17 @@ evalctl is agent-shaped.
259
263
  | --- | --- | --- |
260
264
  | Unit under test | prompt → completion | agent run → files, diffs, command logs, artifacts |
261
265
  | Scoring surface | text of a response | resulting workspace: git diff, expected/forbidden file changes, exit codes, plus text |
262
- | Execution | in-process, synchronous | v0.3 synchronous by default; `run --resume` resumes crashed runs; optional `--queue spoolctl` delegates runner execution |
263
- | Model context | provider API keys | [inferctl](https://inferctl.dev) route/preflight provenance is deferred |
266
+ | Execution | in-process, synchronous | v0.4 synchronous by default; `plan` previews actions, `doctor` diagnoses state, `run --resume` resumes crashed runs; optional `--queue spoolctl` delegates runner execution |
267
+ | Model context | provider API keys | Optional [inferctl](https://inferctl.dev) preflight provenance through `run --inferctl-task`; route capture remains deferred |
264
268
 
265
269
  ## Status
266
270
 
267
- Python pre-release. v0.3 provides scaffold, validate, bounded parallel run
268
- execution, durable run metadata, crash resume, local jobs inspection, optional
269
- spoolctl queueing, status, report, deterministic local scorers, CLI authoring
270
- verbs, execution replay for failed cases, command scorers, truthful
271
- warnings/errors, real schema output, and artifact replay from a copied run
272
- directory.
271
+ Python pre-release. v0.4 provides scaffold, validate, doctor, plan, bounded
272
+ parallel run execution, durable run metadata, crash resume, bounded local jobs
273
+ inspection, optional spoolctl queueing, optional inferctl preflight provenance,
274
+ status, report, deterministic local scorers, CLI authoring verbs, execution
275
+ replay for failed cases, command scorers, truthful warnings/errors, real schema
276
+ output, and artifact replay from a copied run directory.
273
277
  `contract_version` remains `1`.
274
278
 
275
279
  ## Install
@@ -300,8 +304,11 @@ evalctl suite add demo --runner-argv "python3 $EVALCTL_WORKSPACE/r.py" --json
300
304
  evalctl case add demo --task "do X" --workspace fixtures/x --expect-json '{"exact":"ok"}' --json
301
305
  evalctl scorer add demo --name exact --required --json
302
306
  evalctl run demo --json
307
+ evalctl doctor --json
308
+ evalctl plan demo --json
309
+ evalctl run demo --inferctl-task code --json
303
310
  evalctl run --resume <run-id> --json
304
- evalctl jobs list --json
311
+ evalctl jobs list --limit 50 --json
305
312
  evalctl run demo --queue spoolctl --slots 4 --json
306
313
  evalctl replay --failed <run-id> --json
307
314
  evalctl report <run-id> --format json
@@ -322,23 +329,55 @@ reservation is reclaimed by explicit `--resume`. `jobs list|get|prune` inspects
322
329
  completed, running, stale, and orphaned local run state and safely prunes only
323
330
  with explicit confirmation.
324
331
 
332
+ `jobs list` is bounded by default. Use `--limit` and `--cursor` to page through
333
+ large run directories; the JSON envelope includes pagination metadata and a
334
+ paste-ready next-page command when more rows are available.
335
+
325
336
  Durability sidecars are operational state. Reports and artifact replay do not
326
337
  require `run.json`, `.reservation.json`, `.spoolctl.db`, `state.json`, or
327
338
  `job.json`; `report_hash` stays based on the report projection. `SOURCE_DATE_EPOCH`
328
339
  controls `created_ts` for deterministic manifest parity.
329
340
 
341
+ ## Doctor And Plan
342
+
343
+ `evalctl doctor --json` reports runtime, suite root, runs root, reservations,
344
+ spoolctl, inferctl, and runner-safety state without failing just because a
345
+ component is degraded. Use `--component NAME` to scope diagnostics and `--fast`
346
+ for PATH-only optional integration checks.
347
+
348
+ `evalctl plan <suite> --json` resolves the case set without creating run
349
+ directories, enqueueing jobs, executing runners, scoring, or writing inferctl
350
+ artifacts. The plan includes run-id strategy, execution mode, independent-case
351
+ dependency metadata, parallel tracks, per-case actions, warnings, and
352
+ paste-ready follow-up commands. `--resume`, `--queue spoolctl`, `--slots`, and
353
+ `--inferctl-task` mirror the run surface for planning.
354
+
330
355
  ## Optional Spoolctl Queue
331
356
 
332
357
  `evalctl run <suite> --queue spoolctl --json` delegates only runner execution to
333
- spoolctl (`>= 0.4.1`). Evalctl still prepares workspaces, normalizes stdout and
358
+ spoolctl (`>= 0.4.11`, speaking contract `>= 2`). A spoolctl reporting a newer
359
+ contract is accepted. Evalctl still prepares workspaces, normalizes stdout and
334
360
  stderr, captures workspace diffs, scores cases, and writes terminal markers. If
335
361
  spoolctl is absent or incompatible, queued runs fail explicitly; non-queued runs
336
362
  do not need spoolctl.
337
363
 
338
- The queue database is per-run at `.spoolctl.db`; v0.3 starts one ephemeral
364
+ The queue database is per-run at `.spoolctl.db`; v0.4 starts one ephemeral
339
365
  `spoolctl work --drain` worker per queued run. General externally managed worker
340
366
  fleets are not part of this release.
341
367
 
368
+ ## Inferctl Preflight Provenance
369
+
370
+ `evalctl run <suite> --inferctl-task TASK --json` probes inferctl once per run
371
+ and, when compatible preflight support is available, writes per-case
372
+ `inferctl-preflight.json` and `inferctl-provenance.json` before runner execution.
373
+ Queued spoolctl runs capture the same artifacts before enqueue. Absence,
374
+ incompatibility, parse failures, timeouts, and policy/readiness blocks are
375
+ warnings; the runner still executes and scoring proceeds.
376
+
377
+ The v0.4 capture mode is `preflight` only. `inferctl route` is not called, and
378
+ report projection is unchanged, so `report_hash` remains comparable to an
379
+ equivalent run without inferctl.
380
+
342
381
  ## Authoring
343
382
 
344
383
  `suite add`, `case add`, and `scorer add` let agents build a suite without
@@ -11,11 +11,13 @@ durable artifacts. It scores what agents actually do — files written, diffs
11
11
  produced, commands run — on your own machine, with no gateway, dashboard, or
12
12
  SaaS account.
13
13
 
14
- v0.3 writes durable run metadata, supports crash resume, adds local run-state
15
- inspection, and can optionally delegate runner execution to
14
+ v0.4 writes durable run metadata, supports crash resume, adds local run-state
15
+ inspection, diagnoses runtime health, produces side-effect-free execution plans,
16
+ and can optionally delegate runner execution to
16
17
  [spoolctl](https://github.com/Ozhiaki/spoolctl). The standalone synchronous path
17
18
  remains complete and requires no external service. [inferctl](https://inferctl.dev)
18
- route capture remains deferred.
19
+ preflight provenance can be captured before runner execution without changing
20
+ report scoring.
19
21
 
20
22
  ## About
21
23
 
@@ -38,17 +40,17 @@ evalctl is agent-shaped.
38
40
  | --- | --- | --- |
39
41
  | Unit under test | prompt → completion | agent run → files, diffs, command logs, artifacts |
40
42
  | Scoring surface | text of a response | resulting workspace: git diff, expected/forbidden file changes, exit codes, plus text |
41
- | Execution | in-process, synchronous | v0.3 synchronous by default; `run --resume` resumes crashed runs; optional `--queue spoolctl` delegates runner execution |
42
- | Model context | provider API keys | [inferctl](https://inferctl.dev) route/preflight provenance is deferred |
43
+ | Execution | in-process, synchronous | v0.4 synchronous by default; `plan` previews actions, `doctor` diagnoses state, `run --resume` resumes crashed runs; optional `--queue spoolctl` delegates runner execution |
44
+ | Model context | provider API keys | Optional [inferctl](https://inferctl.dev) preflight provenance through `run --inferctl-task`; route capture remains deferred |
43
45
 
44
46
  ## Status
45
47
 
46
- Python pre-release. v0.3 provides scaffold, validate, bounded parallel run
47
- execution, durable run metadata, crash resume, local jobs inspection, optional
48
- spoolctl queueing, status, report, deterministic local scorers, CLI authoring
49
- verbs, execution replay for failed cases, command scorers, truthful
50
- warnings/errors, real schema output, and artifact replay from a copied run
51
- directory.
48
+ Python pre-release. v0.4 provides scaffold, validate, doctor, plan, bounded
49
+ parallel run execution, durable run metadata, crash resume, bounded local jobs
50
+ inspection, optional spoolctl queueing, optional inferctl preflight provenance,
51
+ status, report, deterministic local scorers, CLI authoring verbs, execution
52
+ replay for failed cases, command scorers, truthful warnings/errors, real schema
53
+ output, and artifact replay from a copied run directory.
52
54
  `contract_version` remains `1`.
53
55
 
54
56
  ## Install
@@ -79,8 +81,11 @@ evalctl suite add demo --runner-argv "python3 $EVALCTL_WORKSPACE/r.py" --json
79
81
  evalctl case add demo --task "do X" --workspace fixtures/x --expect-json '{"exact":"ok"}' --json
80
82
  evalctl scorer add demo --name exact --required --json
81
83
  evalctl run demo --json
84
+ evalctl doctor --json
85
+ evalctl plan demo --json
86
+ evalctl run demo --inferctl-task code --json
82
87
  evalctl run --resume <run-id> --json
83
- evalctl jobs list --json
88
+ evalctl jobs list --limit 50 --json
84
89
  evalctl run demo --queue spoolctl --slots 4 --json
85
90
  evalctl replay --failed <run-id> --json
86
91
  evalctl report <run-id> --format json
@@ -101,23 +106,55 @@ reservation is reclaimed by explicit `--resume`. `jobs list|get|prune` inspects
101
106
  completed, running, stale, and orphaned local run state and safely prunes only
102
107
  with explicit confirmation.
103
108
 
109
+ `jobs list` is bounded by default. Use `--limit` and `--cursor` to page through
110
+ large run directories; the JSON envelope includes pagination metadata and a
111
+ paste-ready next-page command when more rows are available.
112
+
104
113
  Durability sidecars are operational state. Reports and artifact replay do not
105
114
  require `run.json`, `.reservation.json`, `.spoolctl.db`, `state.json`, or
106
115
  `job.json`; `report_hash` stays based on the report projection. `SOURCE_DATE_EPOCH`
107
116
  controls `created_ts` for deterministic manifest parity.
108
117
 
118
+ ## Doctor And Plan
119
+
120
+ `evalctl doctor --json` reports runtime, suite root, runs root, reservations,
121
+ spoolctl, inferctl, and runner-safety state without failing just because a
122
+ component is degraded. Use `--component NAME` to scope diagnostics and `--fast`
123
+ for PATH-only optional integration checks.
124
+
125
+ `evalctl plan <suite> --json` resolves the case set without creating run
126
+ directories, enqueueing jobs, executing runners, scoring, or writing inferctl
127
+ artifacts. The plan includes run-id strategy, execution mode, independent-case
128
+ dependency metadata, parallel tracks, per-case actions, warnings, and
129
+ paste-ready follow-up commands. `--resume`, `--queue spoolctl`, `--slots`, and
130
+ `--inferctl-task` mirror the run surface for planning.
131
+
109
132
  ## Optional Spoolctl Queue
110
133
 
111
134
  `evalctl run <suite> --queue spoolctl --json` delegates only runner execution to
112
- spoolctl (`>= 0.4.1`). Evalctl still prepares workspaces, normalizes stdout and
135
+ spoolctl (`>= 0.4.11`, speaking contract `>= 2`). A spoolctl reporting a newer
136
+ contract is accepted. Evalctl still prepares workspaces, normalizes stdout and
113
137
  stderr, captures workspace diffs, scores cases, and writes terminal markers. If
114
138
  spoolctl is absent or incompatible, queued runs fail explicitly; non-queued runs
115
139
  do not need spoolctl.
116
140
 
117
- The queue database is per-run at `.spoolctl.db`; v0.3 starts one ephemeral
141
+ The queue database is per-run at `.spoolctl.db`; v0.4 starts one ephemeral
118
142
  `spoolctl work --drain` worker per queued run. General externally managed worker
119
143
  fleets are not part of this release.
120
144
 
145
+ ## Inferctl Preflight Provenance
146
+
147
+ `evalctl run <suite> --inferctl-task TASK --json` probes inferctl once per run
148
+ and, when compatible preflight support is available, writes per-case
149
+ `inferctl-preflight.json` and `inferctl-provenance.json` before runner execution.
150
+ Queued spoolctl runs capture the same artifacts before enqueue. Absence,
151
+ incompatibility, parse failures, timeouts, and policy/readiness blocks are
152
+ warnings; the runner still executes and scoring proceeds.
153
+
154
+ The v0.4 capture mode is `preflight` only. `inferctl route` is not called, and
155
+ report projection is unchanged, so `report_hash` remains comparable to an
156
+ equivalent run without inferctl.
157
+
121
158
  ## Authoring
122
159
 
123
160
  `suite add`, `case add`, and `scorer add` let agents build a suite without
@@ -1,3 +1,3 @@
1
1
  """evalctl package."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.4.2"
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import stat
7
+ import tempfile
8
+ from pathlib import Path, PurePosixPath
9
+ from typing import Any
10
+
11
+ from .static_contract import EvalctlError, sha256_bytes, sha256_text, stable_json
12
+
13
+
14
+ def read_json(path: Path) -> Any:
15
+ try:
16
+ return json.loads(path.read_text())
17
+ except FileNotFoundError:
18
+ raise EvalctlError("E_SUITE_NOT_FOUND", f"missing file: {path}", f"create {path} or run evalctl init", 1)
19
+ except json.JSONDecodeError as exc:
20
+ raise EvalctlError("E_SCHEMA_VIOLATION", f"invalid JSON in {path}: {exc.msg}", f"fix {path}:{exc.lineno}", 1)
21
+
22
+
23
+ def load_cases(cases_path: Path) -> list[dict[str, Any]]:
24
+ cases: list[dict[str, Any]] = []
25
+ seen: set[str] = set()
26
+ try:
27
+ lines = cases_path.read_text().splitlines()
28
+ except FileNotFoundError:
29
+ raise EvalctlError("E_SUITE_NOT_FOUND", f"missing file: {cases_path}", f"create {cases_path}", 1)
30
+ for line_no, line in enumerate(lines, 1):
31
+ if not line.strip():
32
+ continue
33
+ try:
34
+ case = json.loads(line)
35
+ except json.JSONDecodeError as exc:
36
+ raise EvalctlError("E_CASE_INVALID", f"invalid JSONL at {cases_path}:{line_no}: {exc.msg}", f"fix line {line_no}", 1)
37
+ case_id = case.get("id") or sha256_text(stable_json(case))[7:19]
38
+ if case_id in seen:
39
+ raise EvalctlError("E_CASE_INVALID", f"duplicate case id: {case_id}", "choose unique case ids", 1)
40
+ seen.add(case_id)
41
+ case["id"] = case_id
42
+ cases.append(case)
43
+ return cases
44
+
45
+
46
+ def normalize_rel(path: Path, root: Path) -> str:
47
+ rel = path.relative_to(root)
48
+ parts = rel.parts
49
+ posix = PurePosixPath(*parts).as_posix()
50
+ if posix in ("", ".") or ".." in parts:
51
+ raise ValueError("invalid relative path")
52
+ posix.encode("utf-8", "strict")
53
+ return posix
54
+
55
+
56
+ def display_path_name(path: Path) -> str:
57
+ return os.fsencode(path.name).decode("utf-8", "replace")
58
+
59
+
60
+ def manifest(root: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]:
61
+ warnings: list[dict[str, Any]] = []
62
+ entries: list[dict[str, Any]] = []
63
+ for path in sorted(root.rglob("*"), key=os.fsencode):
64
+ try:
65
+ rel = normalize_rel(path, root)
66
+ st = path.lstat()
67
+ if stat.S_ISDIR(st.st_mode):
68
+ entries.append({"path": rel, "kind": "directory"})
69
+ elif stat.S_ISLNK(st.st_mode):
70
+ target = os.readlink(path)
71
+ entries.append({"path": rel, "kind": "symlink", "target": target, "sha256": sha256_bytes(target.encode("utf-8")), "broken": not path.exists()})
72
+ elif stat.S_ISREG(st.st_mode):
73
+ entries.append({"path": rel, "kind": "file", "sha256": sha256_bytes(path.read_bytes()), "size": st.st_size})
74
+ else:
75
+ subtype = "fifo" if stat.S_ISFIFO(st.st_mode) else "socket" if stat.S_ISSOCK(st.st_mode) else "device" if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode) else "unknown"
76
+ entries.append({"path": rel, "kind": "other", "subtype": subtype})
77
+ except Exception as exc:
78
+ warnings.append({"code": "W_PATH_UNREADABLE", "message": f"could not read path {display_path_name(path)}: {exc}"})
79
+ return {"root": ".", "entries": entries}, warnings
80
+
81
+
82
+ def diff_manifests(before: dict[str, Any], after: dict[str, Any]) -> dict[str, Any]:
83
+ b = {e["path"]: e for e in before["entries"]}
84
+ a = {e["path"]: e for e in after["entries"]}
85
+ changed = []
86
+ for path in sorted(set(b) | set(a)):
87
+ if path not in b:
88
+ changed.append({"path": path, "status": "added", "kind": a[path]["kind"], "sha256_before": None, "sha256_after": a[path].get("sha256")})
89
+ elif path not in a:
90
+ changed.append({"path": path, "status": "deleted", "kind": b[path]["kind"], "sha256_before": b[path].get("sha256"), "sha256_after": None})
91
+ elif b[path].get("sha256") != a[path].get("sha256") or b[path].get("kind") != a[path].get("kind"):
92
+ changed.append({"path": path, "status": "modified", "kind": a[path]["kind"], "sha256_before": b[path].get("sha256"), "sha256_after": a[path].get("sha256")})
93
+ return {"changed_paths": changed}
94
+
95
+
96
+ def apply_redaction(text: str, patterns: list[str], values: list[str]) -> tuple[str, bool]:
97
+ changed = False
98
+ for value in values:
99
+ if value and value in text:
100
+ text = text.replace(value, "[REDACTED]")
101
+ changed = True
102
+ for pattern in patterns:
103
+ text2 = re.sub(pattern, "[REDACTED]", text)
104
+ changed = changed or text2 != text
105
+ text = text2
106
+ return text, changed
107
+
108
+
109
+ def _atomic_write(path: Path, text: str, *, _writer: Any | None = None) -> None:
110
+ writer = _writer or (lambda tmp_path, value: tmp_path.write_text(value))
111
+ tmp_name = None
112
+ with tempfile.NamedTemporaryFile("w", dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", delete=False) as tmp:
113
+ tmp_name = tmp.name
114
+ tmp_path = Path(tmp_name)
115
+ try:
116
+ writer(tmp_path, text)
117
+ os.replace(tmp_path, path)
118
+ finally:
119
+ if tmp_path.exists():
120
+ tmp_path.unlink()
121
+
122
+
123
+ def write_json(path: Path, data: Any) -> None:
124
+ _atomic_write(path, json.dumps(data, indent=2, sort_keys=True) + "\n")
125
+
126
+
127
+ def render_text_diff(diff: dict[str, Any]) -> str:
128
+ return "\n".join(f"{p['status']}\t{p['path']}" for p in diff["changed_paths"]) + ("\n" if diff["changed_paths"] else "")
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import time
5
+
6
+ from .commands import dispatch, report_format_json_requested, wants_json
7
+ from .static_contract import EvalctlError, envelope, stable_json
8
+
9
+
10
+ def print_error(err: EvalctlError, *, json_mode: bool, started: float | None = None) -> int:
11
+ print(err.error["message"], file=sys.stderr)
12
+ if err.error.get("corrected_command"):
13
+ print(f"Did you mean: {err.error['corrected_command']}", file=sys.stderr)
14
+ elif err.error.get("did_you_mean"):
15
+ print(f"Did you mean: {err.error['did_you_mean']}", file=sys.stderr)
16
+ if json_mode:
17
+ print(stable_json(envelope(None, ok=False, errors=[err.error], started=started)))
18
+ return err.exit_code
19
+
20
+
21
+ def main(argv: list[str] | None = None) -> int:
22
+ started = time.time()
23
+ argv = list(sys.argv[1:] if argv is None else argv)
24
+ json_mode = wants_json(argv)
25
+ try:
26
+ return dispatch(argv, json_mode, started)
27
+ except EvalctlError as exc:
28
+ return print_error(exc, json_mode=json_mode or report_format_json_requested(argv), started=started)
29
+ except KeyboardInterrupt:
30
+ return print_error(EvalctlError("E_RUNNER_FAILED", "interrupted", "retry the command", 3), json_mode=json_mode or report_format_json_requested(argv), started=started)
31
+ except Exception as exc:
32
+ return print_error(EvalctlError("E_RUNNER_FAILED", f"internal error: {exc}", "run with --json and inspect errors[0]", 3), json_mode=json_mode or report_format_json_requested(argv), started=started)
33
+
34
+
35
+ if __name__ == "__main__":
36
+ raise SystemExit(main())