Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
51b51ec
fix(run): run and compare exit 2 with a message on a bad spec or snap…
vchamarthi Oct 25, 2026
850f58f
fix(repo): repo run exits 1 with a message on an unreadable batch pin
vchamarthi Oct 25, 2026
d9fba9f
fix(repo): scan exits 1 with a message on a --static or --manifest-fi…
vchamarthi Oct 25, 2026
f18c269
fix(arms): plugin-delta exits 1 with a message on an unreadable arms …
vchamarthi Oct 25, 2026
3b5478b
fix(explore): list and run exit with a message on a malformed task file
vchamarthi Oct 25, 2026
382b2d9
fix(arms): run and matrix-run exit 1 with a message on an unreadable …
vchamarthi Oct 25, 2026
e2e9b83
fix(tasks): run exits 1 with a message when --out-json or --out-md ca…
vchamarthi Oct 25, 2026
57f1f91
fix(repo): report and compare exit 1 with a message on a corrupt batc…
vchamarthi Oct 25, 2026
e7bf343
fix(explore): run exits 1 with a message when --out-json or --out-md …
vchamarthi Oct 25, 2026
fc00a6d
fix(repo): budget and reach exit 1 with a message on an unreadable or…
vchamarthi Oct 25, 2026
eea0ff2
fix(arms): plugin-delta exits 1 with a message when an output cannot …
vchamarthi Oct 25, 2026
4eaef5b
chore: changelog fragment and growth baseline
vchamarthi Oct 26, 2026
51a65ee
Import a public load_benchmark_spec in the run command
antonwolfy Oct 26, 2026
9ed23f4
Exit with a message on unwritable run, compare and arms outputs and o…
vchamarthi Oct 26, 2026
345757b
Report deeply nested JSON in matrix cells and snapshots as an input e…
vchamarthi Oct 26, 2026
14e1738
Split compare_snapshots and match the Error: prefix in benchflow tests
vchamarthi Oct 26, 2026
8d62e91
fix(repo): repo run exits 1 on a deeply nested batch pin
napetrov Oct 26, 2026
71587b3
fix(repo): repo report and compare exit 1 on a result file without a …
vchamarthi Oct 26, 2026
6db68b0
Type _load_snapshot as Any and match the lowercase error: prefix in t…
vchamarthi Oct 26, 2026
89b9aa7
Print the benchflow preflight failure with an Error: prefix in subjects
antonwolfy Oct 26, 2026
48d0d7f
fix(cli): validate compare spec shape, finite snapshot numbers and ce…
vchamarthi Oct 26, 2026
2aff0e3
fix(cli): reject non-finite cell costs, missing score and bad score_t…
vchamarthi Oct 26, 2026
d9582bf
Merge origin/main into fix/cli-input-run-arms-repo
vchamarthi Oct 26, 2026
62a4d94
fix(cli): reject malformed cell fields and check regression threshold…
vchamarthi Oct 26, 2026
6e782be
fix(cli): treat an unreadable batch pin as a message, not a traceback
vchamarthi Oct 26, 2026
d583f54
fix(repos): bound numeric cell fields and cover accepted skills blocks
vchamarthi Oct 26, 2026
40a759e
fix(repos): lower the cell magnitude bound so variance stays finite
antonwolfy Oct 26, 2026
dec3b34
fix(repos): reject score-only cells without a score and pin the bound…
vchamarthi Oct 26, 2026
276795d
test(repos): build the bound-edge cells from _SAFE_MAGNITUDE
antonwolfy Oct 26, 2026
731db39
fix(cli): report unexpected cell shapes and unreadable question sets …
vchamarthi Oct 26, 2026
3782fa3
test(repo): pin each cell-shape error caught by repo report and compare
vchamarthi Oct 27, 2026
d0b3745
fix(repo): record a non-integer agent.num_turns as null
vchamarthi Oct 27, 2026
f71c490
fix(repo): count a finished run as started whatever its num_turns holds
vchamarthi Oct 28, 2026
10151bd
fix(cli): catch deeply nested and non-UTF-8 repo inputs and a missing…
vchamarthi Oct 28, 2026
d471db7
Merge remote-tracking branch 'origin/main' into fix/cli-input-run-arm…
vchamarthi Oct 28, 2026
b8b5278
fix(repo): record a non-finite grader score as a grader failure
xaleryb Oct 29, 2026
883f356
test(repo): pin that a finished run with no tool call is counted
xaleryb Oct 29, 2026
a5af9bc
docs(run): say which exit code an empty root gives with --strict
xaleryb Oct 29, 2026
935e871
Merge intel/main into fix/cli-input-run-arms-repo
xaleryb Oct 29, 2026
333d630
Test that the grader score bound is inclusive at its edge
antonwolfy Oct 29, 2026
f128b52
Word the score-bound grader failure for non-finite and huge scores
antonwolfy Oct 29, 2026
08b9778
fix(arms): matrix run exits 1 when a cell cannot read its inputs
vchamarthi Oct 29, 2026
1f946af
fix(repo): name an unreadable manifest and a bad --static file
vchamarthi Oct 29, 2026
3676296
fix(explore): reject a falsy non-string repo_root and name a task fil…
vchamarthi Oct 29, 2026
142aa94
fix(repo): report a cell field of the wrong shape per cell and read a…
vchamarthi Oct 29, 2026
2ac7e40
fix(run): exit 2 only on a bad spec, snapshot or document selection
vchamarthi Oct 29, 2026
fcf3b10
fix(repo): note a non-integer num_turns in agent.trace_errors
vchamarthi Oct 29, 2026
c6ee9c3
test(repo): compare the batch path in a platform-independent form
vchamarthi Oct 29, 2026
dd7d4e6
docs(changelog): list the fixed bad-input cases
vchamarthi Oct 29, 2026
13dd420
fix(repo): leave a process counter or grade metric beyond the bound o…
vchamarthi Oct 29, 2026
4c6f15b
fix(explore): name the task file when a reference's lines range is in…
vchamarthi Oct 29, 2026
9e8067f
fix(tasks): prefix the harness validation exit with Error: and test a…
vchamarthi Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dokimi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ The default static benchmark is configured in `benchmarks/spec.v1.yaml` and curr
| `readability` | Estimates text readability with a Flesch-Kincaid-style score. |
| `example_pass_rate` | Executes supported fenced code examples in isolation. |

Weights are normalized across enabled metrics. The benchmark also supports soft gates, hard gates, critical bands, and regression thresholds. Hard gates and critical bands are enforced only when `--strict` is passed. `--strict` also exits 1 when no document was measured.
Weights are normalized across enabled metrics. The benchmark also supports soft gates, hard gates, critical bands, and regression thresholds. Hard gates and critical bands are enforced only when `--strict` is passed. With `--strict`, a spec without a `golden_manifest` also exits 1 when no document was measured. A spec with one, as `spec.v1.yaml` has, exits 2 with or without `--strict` when fewer documents than its `min_docs` match, none included. An empty or absolute `include` pattern exits 2 as well. Exit 1 is not only the `--strict` verdict: a document that cannot be read, or an `--out-json` or `--out-md` path that cannot be written, also exits 1, with an `Error:` or `❌` line on stderr saying which.

## LLM and treatment evaluation workflow

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Fixed

- **`run`, `compare`, `arms`, `tasks`, `explore` and `repo`** exit with a message instead of a traceback on the unreadable, non-UTF-8 or corrupt spec, snapshot, result, plan, task and question files their tests cover, and on an unwritable output. This covers a deeply nested `--manifest-file`, `--static` or `--harness` file, an unreadable or non-UTF-8 `.agent-eval/repo-eval.yaml`, a `tasks run --tasks` name that matches no task, and an `explore` task file with an invalid YAML timestamp, a bad `lines` range or a `repo_root` that is not a string.
- `run` and `compare` exit 2 on a bad spec or snapshot (a non-numeric summary value, an empty or absolute `golden_manifest` include pattern, or fewer documents than its `min_docs`) and exit 1 when an output cannot be written, as `arms` does. An error inside a metric is no longer reported as bad input. `tasks` error messages use the `Error:` prefix.
- `repo report` and `repo compare` exit 1 on a result file without a usable `grade`, `agent`, `task`, `arm` or `model`, naming the file. A grader score that is NaN, infinite or beyond 1e150 is recorded as a grader failure, and a batch that already stores one is read the same way. A `process` counter or `grade.json` metric that is not finite or beyond 1e150 is left out of the means, as a non-number is, instead of ending in an overflow traceback.
- `repo run` records `agent.num_turns` as null when the harness reports anything but an integer, and notes it in `agent.trace_errors`; a finished run with such a value still counts as started.
65 changes: 44 additions & 21 deletions dokimi/dokimi/commands/arms.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def cmd_arms_run(args: argparse.Namespace) -> None:
output = _build_arms_output(args, specs, questions)
except EmptyQuestionError:
raise # a bad input record, not an arm setup error: cli.main reports it (exit 2)
except (ValueError, FileNotFoundError) as exc:
except (ValueError, OSError, RecursionError) as exc:
print(f"Error building arms/plugins: {exc}", file=sys.stderr)
sys.exit(1)

Expand All @@ -108,7 +108,6 @@ def cmd_arms_run(args: argparse.Namespace) -> None:

def cmd_arms_matrix_run(args: argparse.Namespace) -> None:
"""Run every cell in a matrix.cells descriptor and write a rollup."""
from dokimi.artifacts import save_artifact
from dokimi.eval.cells import load_matrix_cells
from dokimi.eval.matrix_rollup import build_matrix_rollup, strip_internal_payloads
from dokimi.report.matrix_rollup_report import render_matrix_rollup_report
Expand All @@ -124,14 +123,17 @@ def cmd_arms_matrix_run(args: argparse.Namespace) -> None:
cell_args = argparse.Namespace(**vars(args))
_apply_matrix_cell(cell_args, cell)
_check_sampling_support(cell_args)
except (ValueError, FileNotFoundError) as exc:
except (ValueError, OSError, RecursionError) as exc:
print(f"Error loading matrix run inputs: {exc}", file=sys.stderr)
sys.exit(1)

out_dir = Path(args.out_dir) if args.out_dir else Path(f"results/arms/{args.product}-matrix")
out_dir.mkdir(parents=True, exist_ok=True)
cells_dir = out_dir / "cells"
cells_dir.mkdir(parents=True, exist_ok=True)
try:
cells_dir.mkdir(parents=True, exist_ok=True)
except OSError as exc:
print(f"Error: cannot write {cells_dir}: {exc}", file=sys.stderr)
sys.exit(1)
cell_artifacts = []
for cell in cells:
cell_args = argparse.Namespace(**vars(args))
Expand All @@ -143,7 +145,7 @@ def cmd_arms_matrix_run(args: argparse.Namespace) -> None:
output = _build_arms_output(cell_args, specs, questions)
except EmptyQuestionError:
raise # a bad input record, not a cell error: cli.main reports it (exit 2)
except (ValueError, FileNotFoundError) as exc:
except (ValueError, OSError, RecursionError) as exc:
print(f"Error running matrix cell {cell.name!r}: {exc}", file=sys.stderr)
sys.exit(1)
_save_arms_output(output, out_json, out_md)
Expand All @@ -160,24 +162,22 @@ def cmd_arms_matrix_run(args: argparse.Namespace) -> None:
if not payload:
continue
delta_json = Path(row["artifact"])
save_artifact("plugin_delta", payload, delta_json)
delta_md = delta_json.with_suffix(".md")
delta_md.write_text(render_plugin_delta_report(payload), encoding="utf-8")
_save_artifact_or_exit("plugin_delta", payload, delta_json)
_write_report(delta_json.with_suffix(".md"), render_plugin_delta_report(payload))
print(f"OK Saved plugin delta: {delta_json}")

persisted = strip_internal_payloads(rollup)
out_json = Path(args.out_json) if args.out_json else out_dir / "matrix-rollup.json"
out_md = Path(args.out_md) if args.out_md else out_json.with_suffix(".md")
save_artifact("matrix_rollup", persisted, out_json)
out_md.parent.mkdir(parents=True, exist_ok=True)
out_md.write_text(render_matrix_rollup_report(persisted), encoding="utf-8")
_save_artifact_or_exit("matrix_rollup", persisted, out_json)
_write_report(out_md, render_matrix_rollup_report(persisted))
print(f"OK Saved matrix rollup: {out_json}")
print(f"OK Saved matrix report: {out_md}")


def cmd_arms_plugin_delta(args: argparse.Namespace) -> None:
"""Compare paired no-plugin/plugin arms artifacts."""
from dokimi.artifacts import load_artifact, save_artifact
from dokimi.artifacts import load_artifact
from dokimi.eval.plugin_delta import PluginDeltaError, compare_plugin_runs
from dokimi.records import records
from dokimi.report.plugin_delta_report import render_plugin_delta_report
Expand All @@ -189,17 +189,16 @@ def cmd_arms_plugin_delta(args: argparse.Namespace) -> None:
for section in ("answers", "evaluations"):
records(artifact.get(section) or [], source=f"{path} {section}", kind=section)
output = compare_plugin_runs(baseline, plugin)
except (PluginDeltaError, ValueError, FileNotFoundError) as exc:
except (PluginDeltaError, ValueError, OSError, RecursionError) as exc:
print(f"Error computing plugin delta: {exc}", file=sys.stderr)
sys.exit(1)

out_json = Path(args.out_json) if args.out_json else Path("results/arms/plugin_delta.json")
save_artifact("plugin_delta", output, out_json)
_save_artifact_or_exit("plugin_delta", output, out_json)
print(f"OK Saved plugin delta: {out_json}")

out_md = Path(args.out_md) if args.out_md else out_json.with_suffix(".md")
out_md.parent.mkdir(parents=True, exist_ok=True)
out_md.write_text(render_plugin_delta_report(output), encoding="utf-8")
_write_report(out_md, render_plugin_delta_report(output))
print(f"OK Saved plugin delta report: {out_md}")

print(
Expand Down Expand Up @@ -485,7 +484,7 @@ def _refuse_external_trigger_set(path: str) -> None:

try:
data = json.loads(Path(path).read_text(encoding="utf-8"))
except (OSError, ValueError):
except (OSError, ValueError, RecursionError):
return # the questions loader reports it
fmt = detect_trigger_format(data)
if fmt and fmt != FORMAT_QUESTIONS:
Expand Down Expand Up @@ -726,14 +725,38 @@ def _build_arms_output(args: argparse.Namespace, specs: list[str], questions: li
)


def _write_report(path: Path, text: str) -> None:
"""Write a Markdown report, or exit 1 naming ``path`` when it cannot be written."""
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
except OSError as exc:
print(f"Error: cannot write {path}: {exc}", file=sys.stderr)
sys.exit(1)


def _save_artifact_or_exit(kind: str, payload: dict, path: Path) -> None:
Comment thread
vchamarthi marked this conversation as resolved.
"""``save_artifact``, or exit 1 naming ``path`` when it cannot be written."""
from dokimi.artifacts import save_artifact

try:
save_artifact(kind, payload, path)
except OSError as exc:
print(f"Error: cannot write {path}: {exc}", file=sys.stderr)
sys.exit(1)


def _save_arms_output(output: dict, out_json: Path, out_md: Path) -> None:
Comment thread
vchamarthi marked this conversation as resolved.
from dokimi.eval.arm_runner import ArmRunner
from dokimi.report.arms_report import render_arms_report

ArmRunner.save(output, out_json)
try:
ArmRunner.save(output, out_json)
except OSError as exc:
print(f"Error: cannot write {out_json}: {exc}", file=sys.stderr)
sys.exit(1)
print(f"OK Saved arms comparison: {out_json}")
out_md.parent.mkdir(parents=True, exist_ok=True)
out_md.write_text(render_arms_report(output), encoding="utf-8")
_write_report(out_md, render_arms_report(output))
print(f"OK Saved arms report: {out_md}")


Expand Down
15 changes: 10 additions & 5 deletions dokimi/dokimi/commands/explore.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import annotations

import argparse
import sys
from pathlib import Path

_TASKS_ROOT_HELP = (
Expand All @@ -26,7 +27,7 @@ def cmd_explore_list(args: argparse.Namespace) -> None:
default_repo_root = Path(args.repo_root) if getattr(args, "repo_root", None) else None
try:
tasks = discover_explore_tasks(root, default_repo_root=default_repo_root)
except FileNotFoundError as exc:
except (FileNotFoundError, ValueError) as exc: # a task file that cannot be read or parsed
raise SystemExit(str(exc)) from exc
for task in tasks:
n_refs = len(task.references)
Expand Down Expand Up @@ -90,7 +91,7 @@ def cmd_explore_run(args: argparse.Namespace) -> None:
tasks = [
load_explore_task(t, root=root, default_repo_root=default_repo_root) for t in ids
]
except FileNotFoundError as exc:
except (FileNotFoundError, ValueError) as exc: # a task file that cannot be read or parsed
raise SystemExit(str(exc)) from exc
if not tasks:
raise SystemExit("No tasks selected. Pass --tasks <id[,id]> or --all.")
Expand All @@ -111,14 +112,18 @@ def cmd_explore_run(args: argparse.Namespace) -> None:
output = runner.run()

out_json = Path(args.out_json) if args.out_json else output_root / "explore_runs.json"
save_artifact("explore_runs", output, out_json)
try:
save_artifact("explore_runs", output, out_json)
except OSError as exc:
print(f"Error: cannot write {out_json}: {exc}", file=sys.stderr)
raise SystemExit(1) from exc
print(f"Saved explore run artifact: {out_json}")

if args.out_md:
from dokimi.report.exploration_report import render_explore_report
from dokimi.utils import write_text_or_exit

Path(args.out_md).parent.mkdir(parents=True, exist_ok=True)
Path(args.out_md).write_text(render_explore_report(output), encoding="utf-8")
write_text_or_exit(Path(args.out_md), render_explore_report(output))
print(f"Saved exploration report: {args.out_md}")

for arm, stats in output["summary"]["per_arm"].items():
Expand Down
15 changes: 12 additions & 3 deletions dokimi/dokimi/commands/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import re
import sys
from pathlib import Path
from typing import Any


def _normalize_whitespace(text: str, max_len: int | None = 100) -> str:
Expand Down Expand Up @@ -73,6 +74,14 @@ def cmd_repo_validate(args: argparse.Namespace) -> None:
sys.exit(1)


def _read_static(path: str) -> Any:
try:
return json.loads(Path(path).read_text(encoding="utf-8"))
except (OSError, ValueError, RecursionError) as exc: # ValueError: bad JSON or UTF-8
print(f"✗ --static {path}: {exc}", file=sys.stderr)
sys.exit(1)


def cmd_repo_scan(args: argparse.Namespace) -> None:
"""L0: check every mechanically checkable claim in the guidance against the same tree."""
from dokimi.artifacts import save_artifact, stamp
Expand All @@ -82,10 +91,10 @@ def cmd_repo_scan(args: argparse.Namespace) -> None:
try:
tree = open_source(RepoSource.parse(args.source), _cache_dir(args))
manifest = _load_manifest(tree, args, guidance_default=True)
static = json.loads(Path(args.static).read_text(encoding="utf-8")) if args.static else None
except (SourceError, ManifestError, OSError, json.JSONDecodeError) as exc:
except (SourceError, ManifestError, OSError, ValueError, RecursionError) as exc:
print(f"✗ {exc}", file=sys.stderr)
sys.exit(1)
static = _read_static(args.static) if args.static else None
payload = scan_repo(tree, manifest, static)
if args.out:
try:
Expand Down Expand Up @@ -314,7 +323,7 @@ def _load_manifest(tree, args: argparse.Namespace, *, guidance_default: bool = F
return load_manifest(tree, path)
try:
raw = yaml.safe_load(Path(args.manifest_file).read_text(encoding="utf-8"))
except (OSError, yaml.YAMLError) as exc:
except (OSError, ValueError, RecursionError, yaml.YAMLError) as exc: # ValueError: not UTF-8
raise ManifestError(args.manifest_file, [str(exc)]) from exc
return parse_manifest(raw, path)

Expand Down
20 changes: 15 additions & 5 deletions dokimi/dokimi/commands/repo_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,13 @@ def _uses_builtin_graders(content: Path, manifest: RepoManifest) -> bool:

def _pin_batch(batch_dir: Path, pin: dict[str, str]) -> None:
path = batch_dir / "_eval" / "batch.json"
if path.is_file():
old = json.loads(path.read_text(encoding="utf-8"))
if path.exists():
try:
old = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError, RecursionError) as exc: # unreadable, bad JSON or UTF-8, deep nesting
old = exc
if not isinstance(old, dict):
raise ValueError(f"batch {batch_dir.name} has an unreadable pin ({path}); use another --batch")
changed = [k for k in pin if old.get(k) != pin[k]]
if changed:
was = ", ".join(f"{k} {old.get(k)}" for k in changed)
Expand Down Expand Up @@ -379,19 +384,24 @@ def _harness_bin(name: str) -> Path:

def cmd_repo_report(args: argparse.Namespace) -> None:
"""Per (task, arm, model): gated success, grader failures, missing cells, cost, turns."""
from dokimi.repos.report import format_report, summarize
from dokimi.repos.report import CellFileError, format_report, summarize

batch_dir = Path(args.batch_dir)
if not batch_dir.is_dir():
print(f"✗ not a directory: {batch_dir}", file=sys.stderr)
sys.exit(1)
summary = summarize(batch_dir)
try:
summary = summarize(batch_dir)
except CellFileError as exc:
print(f"✗ {exc}", file=sys.stderr)
sys.exit(1)
print(json.dumps(summary, indent=2) if args.json else format_report(summary))


def cmd_repo_compare(args: argparse.Namespace) -> None:
"""Per (treatment, model, measure): per-task deltas against the baseline, 95% CI, sign-flip p."""
from dokimi.repos.compare import CompareError, compare, format_compare
from dokimi.repos.report import CellFileError

dirs = [Path(d) for d in args.batch_dirs]
for d in dirs:
Expand All @@ -402,7 +412,7 @@ def cmd_repo_compare(args: argparse.Namespace) -> None:
try:
summary = compare(dirs, args.baseline, arms, args.model, tuple(args.metric or ()),
pass_k=args.pass_k, path_stability=args.path_stability)
except CompareError as exc:
except (CompareError, CellFileError) as exc:
print(f"✗ {exc}", file=sys.stderr)
sys.exit(1)
print(json.dumps(summary, indent=2) if args.json else format_compare(summary))
Expand Down
Loading
Loading