runledger 0.2.2__tar.gz → 0.2.3__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 (55) hide show
  1. {runledger-0.2.2/src/runledger.egg-info → runledger-0.2.3}/PKG-INFO +11 -20
  2. {runledger-0.2.2 → runledger-0.2.3}/README.md +10 -19
  3. {runledger-0.2.2 → runledger-0.2.3}/pyproject.toml +1 -1
  4. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/__init__.py +1 -1
  5. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/cli.py +1 -1
  6. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/config/loader.py +54 -14
  7. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/reporting/failures.py +1 -1
  8. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/runner/subprocess.py +25 -0
  9. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/util/redaction.py +4 -4
  10. {runledger-0.2.2 → runledger-0.2.3/src/runledger.egg-info}/PKG-INFO +11 -20
  11. {runledger-0.2.2 → runledger-0.2.3}/LICENSE +0 -0
  12. {runledger-0.2.2 → runledger-0.2.3}/setup.cfg +0 -0
  13. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/__main__.py +0 -0
  14. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/artifacts/__init__.py +0 -0
  15. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/artifacts/junit.py +0 -0
  16. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/artifacts/report.py +0 -0
  17. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/artifacts/run_log.py +0 -0
  18. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/artifacts/summary.py +0 -0
  19. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/assertions/__init__.py +0 -0
  20. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/assertions/base.py +0 -0
  21. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/assertions/engine.py +0 -0
  22. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/assertions/json_schema.py +0 -0
  23. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/assertions/required_fields.py +0 -0
  24. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/assertions/tool_contract.py +0 -0
  25. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/baseline/__init__.py +0 -0
  26. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/baseline/io.py +0 -0
  27. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/baseline/models.py +0 -0
  28. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/cassette/__init__.py +0 -0
  29. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/cassette/loader.py +0 -0
  30. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/cassette/match.py +0 -0
  31. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/cassette/models.py +0 -0
  32. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/cassette/writer.py +0 -0
  33. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/config/__init__.py +0 -0
  34. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/config/models.py +0 -0
  35. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/protocol/__init__.py +0 -0
  36. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/protocol/jsonl.py +0 -0
  37. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/protocol/messages.py +0 -0
  38. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/regression/__init__.py +0 -0
  39. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/regression/engine.py +0 -0
  40. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/reporting/__init__.py +0 -0
  41. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/runner/__init__.py +0 -0
  42. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/runner/budgets.py +0 -0
  43. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/runner/engine.py +0 -0
  44. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/runner/models.py +0 -0
  45. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/tools/__init__.py +0 -0
  46. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/tools/builtin.py +0 -0
  47. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/tools/registry.py +0 -0
  48. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/util/__init__.py +0 -0
  49. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/util/canonical_json.py +0 -0
  50. {runledger-0.2.2 → runledger-0.2.3}/src/runledger/util/normalize.py +0 -0
  51. {runledger-0.2.2 → runledger-0.2.3}/src/runledger.egg-info/SOURCES.txt +0 -0
  52. {runledger-0.2.2 → runledger-0.2.3}/src/runledger.egg-info/dependency_links.txt +0 -0
  53. {runledger-0.2.2 → runledger-0.2.3}/src/runledger.egg-info/entry_points.txt +0 -0
  54. {runledger-0.2.2 → runledger-0.2.3}/src/runledger.egg-info/requires.txt +0 -0
  55. {runledger-0.2.2 → runledger-0.2.3}/src/runledger.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runledger
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: CI for tool-using agents: deterministic eval suites with record/replay, hard assertions, budgets, and PR regression gates.
5
5
  Author: RunLedger
6
6
  License-Expression: MIT
@@ -69,13 +69,13 @@ The same check as a GitHub Action. `suite.yaml` already points at `baselines/dem
69
69
 
70
70
  ```yaml
71
71
  - id: evals
72
- uses: runledger/Runledger@v0.2
72
+ uses: runledger/Runledger@v0.2.3
73
73
  with:
74
74
  path: ./evals/demo
75
75
  mode: replay
76
76
  ```
77
77
 
78
- This repo's `action.yml` also accepts a `baseline` input and sets `result` (`pass` or `fail`), `exit-code`, and `artifacts-path`. Those fields ship when a release containing this file is tagged. The in-repo self-test calls the action with `uses: ./`.
78
+ `runledger/Runledger@v0.2.3` accepts a `baseline` input and sets `result` (`pass` or `fail`), `exit-code`, and `artifacts-path`. The in-repo self-test calls the action with `uses: ./`.
79
79
 
80
80
  Page copy for runledger.io lives in `docs/site-quickstart.md`.
81
81
 
@@ -229,7 +229,7 @@ Optional:
229
229
 
230
230
  ```yaml
231
231
  suite_name: support-triage
232
- agent_command: ["python", "agent.py"]
232
+ agent_command: ["python3", "agent.py"]
233
233
  mode: replay # replay | record | live
234
234
  cases_path: cases
235
235
  tool_registry:
@@ -306,13 +306,10 @@ Each line is one tool invocation:
306
306
  MVP assertions:
307
307
 
308
308
  * `json_schema` (validate final output with JSON Schema)
309
- * `required_fields` (keys exist / basic typing)
310
- * `regex` / `contains` (for specific fields)
311
- * `tool_contract`:
312
-
313
- * must call tool X
314
- * must not call tool Y
315
- * X before Y (ordering)
309
+ * `required_fields` (keys exist)
310
+ * `must_call` (must call tool X)
311
+ * `must_not_call` (must not call tool Y)
312
+ * `call_order` (X before Y; tools do not have to be adjacent)
316
313
 
317
314
  Not default-gating (optional later):
318
315
 
@@ -323,18 +320,12 @@ Not default-gating (optional later):
323
320
 
324
321
  ## Budgets (merge gates)
325
322
 
326
- MVP budgets:
323
+ Budgets that fail a run:
327
324
 
328
325
  * `max_wall_ms`
329
326
  * `max_tool_calls`
330
327
  * `max_tool_errors`
331
328
 
332
- Not yet enforced (accepted in config, reported in artifacts when the agent sends metrics, but
333
- they do not fail a run today):
334
-
335
- * `max_tokens_out`
336
- * `max_cost_usd`
337
-
338
329
  ---
339
330
 
340
331
  ## Baselines + regression checks
@@ -389,7 +380,7 @@ jobs:
389
380
 
390
381
  - name: Run deterministic evals (replay)
391
382
  id: evals
392
- uses: runledger/Runledger@v0.2
383
+ uses: runledger/Runledger@v0.2.3
393
384
  with:
394
385
  path: ./evals/demo
395
386
  mode: replay
@@ -398,7 +389,7 @@ jobs:
398
389
  run: echo "result=${{ steps.evals.outputs.result }} exit=${{ steps.evals.outputs.exit-code }}"
399
390
  ```
400
391
 
401
- `runledger init` writes `.github/workflows/runledger.yml`, which installs the CLI and runs the suite with `--baseline`. The action above is the Marketplace entry in `action.yml`. It uploads `runledger_out/**` itself. On a release that contains this `action.yml`, the step also sets `result`, `exit-code`, and `artifacts-path`, and you can pass `baseline:` to override `suite.yaml`.
392
+ `runledger init` writes `.github/workflows/runledger.yml`, which installs the CLI and runs the suite with `--baseline`. The action above is the Marketplace entry in `action.yml` at `runledger/Runledger@v0.2.3`. It uploads `runledger_out/**` on success and on failure. The step sets `result`, `exit-code`, and `artifacts-path`, and you can pass `baseline:` to override `suite.yaml`. A failing run fails the job.
402
393
 
403
394
  ---
404
395
 
@@ -41,13 +41,13 @@ The same check as a GitHub Action. `suite.yaml` already points at `baselines/dem
41
41
 
42
42
  ```yaml
43
43
  - id: evals
44
- uses: runledger/Runledger@v0.2
44
+ uses: runledger/Runledger@v0.2.3
45
45
  with:
46
46
  path: ./evals/demo
47
47
  mode: replay
48
48
  ```
49
49
 
50
- This repo's `action.yml` also accepts a `baseline` input and sets `result` (`pass` or `fail`), `exit-code`, and `artifacts-path`. Those fields ship when a release containing this file is tagged. The in-repo self-test calls the action with `uses: ./`.
50
+ `runledger/Runledger@v0.2.3` accepts a `baseline` input and sets `result` (`pass` or `fail`), `exit-code`, and `artifacts-path`. The in-repo self-test calls the action with `uses: ./`.
51
51
 
52
52
  Page copy for runledger.io lives in `docs/site-quickstart.md`.
53
53
 
@@ -201,7 +201,7 @@ Optional:
201
201
 
202
202
  ```yaml
203
203
  suite_name: support-triage
204
- agent_command: ["python", "agent.py"]
204
+ agent_command: ["python3", "agent.py"]
205
205
  mode: replay # replay | record | live
206
206
  cases_path: cases
207
207
  tool_registry:
@@ -278,13 +278,10 @@ Each line is one tool invocation:
278
278
  MVP assertions:
279
279
 
280
280
  * `json_schema` (validate final output with JSON Schema)
281
- * `required_fields` (keys exist / basic typing)
282
- * `regex` / `contains` (for specific fields)
283
- * `tool_contract`:
284
-
285
- * must call tool X
286
- * must not call tool Y
287
- * X before Y (ordering)
281
+ * `required_fields` (keys exist)
282
+ * `must_call` (must call tool X)
283
+ * `must_not_call` (must not call tool Y)
284
+ * `call_order` (X before Y; tools do not have to be adjacent)
288
285
 
289
286
  Not default-gating (optional later):
290
287
 
@@ -295,18 +292,12 @@ Not default-gating (optional later):
295
292
 
296
293
  ## Budgets (merge gates)
297
294
 
298
- MVP budgets:
295
+ Budgets that fail a run:
299
296
 
300
297
  * `max_wall_ms`
301
298
  * `max_tool_calls`
302
299
  * `max_tool_errors`
303
300
 
304
- Not yet enforced (accepted in config, reported in artifacts when the agent sends metrics, but
305
- they do not fail a run today):
306
-
307
- * `max_tokens_out`
308
- * `max_cost_usd`
309
-
310
301
  ---
311
302
 
312
303
  ## Baselines + regression checks
@@ -361,7 +352,7 @@ jobs:
361
352
 
362
353
  - name: Run deterministic evals (replay)
363
354
  id: evals
364
- uses: runledger/Runledger@v0.2
355
+ uses: runledger/Runledger@v0.2.3
365
356
  with:
366
357
  path: ./evals/demo
367
358
  mode: replay
@@ -370,7 +361,7 @@ jobs:
370
361
  run: echo "result=${{ steps.evals.outputs.result }} exit=${{ steps.evals.outputs.exit-code }}"
371
362
  ```
372
363
 
373
- `runledger init` writes `.github/workflows/runledger.yml`, which installs the CLI and runs the suite with `--baseline`. The action above is the Marketplace entry in `action.yml`. It uploads `runledger_out/**` itself. On a release that contains this `action.yml`, the step also sets `result`, `exit-code`, and `artifacts-path`, and you can pass `baseline:` to override `suite.yaml`.
364
+ `runledger init` writes `.github/workflows/runledger.yml`, which installs the CLI and runs the suite with `--baseline`. The action above is the Marketplace entry in `action.yml` at `runledger/Runledger@v0.2.3`. It uploads `runledger_out/**` on success and on failure. The step sets `result`, `exit-code`, and `artifacts-path`, and you can pass `baseline:` to override `suite.yaml`. A failing run fails the job.
374
365
 
375
366
  ---
376
367
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "runledger"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "CI for tool-using agents: deterministic eval suites with record/replay, hard assertions, budgets, and PR regression gates."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.3"
@@ -498,7 +498,7 @@ def init(
498
498
  " runledger run "
499
499
  f'"{_posix_rel(evals_dir, cwd)}" --mode replay --baseline "{_posix_rel(baseline_file, cwd)}"'
500
500
  )
501
- console.print(f" open runledger_out/{suite}/<run_id>/report.html")
501
+ console.print(f" report: runledger_out/{suite}/<run_id>/report.html")
502
502
  console.print(f" commit {_display_path(workflow_path, base_dir=cwd)}")
503
503
 
504
504
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
3
4
  from pathlib import Path
4
5
  from typing import Any
5
6
 
@@ -7,12 +8,33 @@ import yaml
7
8
 
8
9
  from .models import CaseConfig, SuiteConfig
9
10
 
11
+ _WINDOWS_DRIVE = re.compile(r"^[A-Za-z]:[\\/]")
12
+
13
+
14
+ def normalize_config_path(value: str) -> str:
15
+ """Turn Windows separators into POSIX separators.
16
+
17
+ Suites authored on Windows often store ``agent\\agent.py`` and
18
+ ``cassettes\\t1.jsonl``. Those strings do not resolve on the Linux runners
19
+ used by GitHub Actions. Forward slashes work on both.
20
+ """
21
+ if "\\" not in value:
22
+ return value
23
+ return value.replace("\\", "/")
24
+
25
+
26
+ def _is_absolute_config_path(value: str) -> bool:
27
+ if Path(value).is_absolute():
28
+ return True
29
+ return bool(_WINDOWS_DRIVE.match(value)) or value.startswith("//")
30
+
10
31
 
11
32
  def _load_yaml(path: Path) -> dict[str, Any]:
12
33
  try:
13
34
  data = yaml.safe_load(path.read_text(encoding="utf-8"))
14
35
  except yaml.YAMLError as exc:
15
- raise ValueError(f"Invalid YAML in {path}") from exc
36
+ detail = str(exc).strip() or "could not parse the file"
37
+ raise ValueError(f"Invalid YAML in {path}: {detail}") from exc
16
38
  except OSError as exc:
17
39
  raise FileNotFoundError(f"Unable to read {path}") from exc
18
40
 
@@ -28,8 +50,11 @@ def _resolve_schema_paths(assertions: list[object], base_dir: Path) -> None:
28
50
  if entry.get("type") != "json_schema":
29
51
  continue
30
52
  schema_path = entry.get("schema_path")
31
- if isinstance(schema_path, str) and not Path(schema_path).is_absolute():
32
- entry["schema_path"] = str((base_dir / schema_path).resolve())
53
+ if isinstance(schema_path, str):
54
+ schema_path = normalize_config_path(schema_path)
55
+ if not _is_absolute_config_path(schema_path):
56
+ schema_path = str((base_dir / schema_path).resolve())
57
+ entry["schema_path"] = schema_path
33
58
 
34
59
 
35
60
  def load_suite(path: Path) -> SuiteConfig:
@@ -39,31 +64,43 @@ def load_suite(path: Path) -> SuiteConfig:
39
64
  if not suite_path.is_file():
40
65
  raise FileNotFoundError(f"Suite file not found: {suite_path}")
41
66
  data = _load_yaml(suite_path)
67
+ cases_path = data.get("cases_path")
68
+ if isinstance(cases_path, str):
69
+ data["cases_path"] = normalize_config_path(cases_path)
42
70
  agent_command = data.get("agent_command")
43
71
  if isinstance(agent_command, list):
44
72
  resolved_command: list[object] = []
45
73
  for part in agent_command:
46
- if isinstance(part, str) and not Path(part).is_absolute():
47
- candidate = (suite_path.parent / part).resolve()
48
- if candidate.exists():
49
- resolved_command.append(str(candidate))
50
- continue
74
+ if isinstance(part, str):
75
+ part = normalize_config_path(part)
76
+ if not _is_absolute_config_path(part):
77
+ candidate = (suite_path.parent / part).resolve()
78
+ if candidate.exists():
79
+ resolved_command.append(str(candidate))
80
+ continue
51
81
  resolved_command.append(part)
52
82
  data["agent_command"] = resolved_command
53
83
  assertions = data.get("assertions", [])
54
84
  if isinstance(assertions, list):
55
85
  _resolve_schema_paths(assertions, suite_path.parent)
56
86
  baseline_path = data.get("baseline_path")
57
- if isinstance(baseline_path, str) and not Path(baseline_path).is_absolute():
58
- data["baseline_path"] = str((suite_path.parent / baseline_path).resolve())
87
+ if isinstance(baseline_path, str):
88
+ baseline_path = normalize_config_path(baseline_path)
89
+ if not _is_absolute_config_path(baseline_path):
90
+ baseline_path = str((suite_path.parent / baseline_path).resolve())
91
+ data["baseline_path"] = baseline_path
59
92
  output_dir = data.get("output_dir")
60
- if isinstance(output_dir, str) and not Path(output_dir).is_absolute():
61
- data["output_dir"] = str((suite_path.parent / output_dir).resolve())
93
+ if isinstance(output_dir, str):
94
+ output_dir = normalize_config_path(output_dir)
95
+ if not _is_absolute_config_path(output_dir):
96
+ output_dir = str((suite_path.parent / output_dir).resolve())
97
+ data["output_dir"] = output_dir
62
98
  return SuiteConfig.model_validate(data)
63
99
 
64
100
 
65
101
  def load_cases(suite_dir: Path, cases_path: str) -> list[CaseConfig]:
66
102
  """Load cases with cassette paths resolved relative to the suite directory."""
103
+ cases_path = normalize_config_path(cases_path)
67
104
  cases_dir = (suite_dir / cases_path).resolve()
68
105
  if not cases_dir.is_dir():
69
106
  raise FileNotFoundError(f"Cases directory not found: {cases_dir}")
@@ -76,8 +113,11 @@ def load_cases(suite_dir: Path, cases_path: str) -> list[CaseConfig]:
76
113
  for case_file in case_files:
77
114
  data = _load_yaml(case_file)
78
115
  cassette_value = data.get("cassette")
79
- if isinstance(cassette_value, str) and not Path(cassette_value).is_absolute():
80
- data["cassette"] = str((suite_dir / cassette_value).resolve())
116
+ if isinstance(cassette_value, str):
117
+ cassette_value = normalize_config_path(cassette_value)
118
+ if not _is_absolute_config_path(cassette_value):
119
+ cassette_value = str((suite_dir / cassette_value).resolve())
120
+ data["cassette"] = cassette_value
81
121
  assertions = data.get("assertions")
82
122
  if isinstance(assertions, list):
83
123
  _resolve_schema_paths(assertions, suite_dir)
@@ -63,7 +63,7 @@ def _expected_observed(assertion_type: str, details: Mapping[str, Any] | None) -
63
63
  if assertion_type == "required_fields":
64
64
  missing = details.get("missing")
65
65
  return (
66
- f"fields present: {_join_names(missing)}",
66
+ f"required fields: {_join_names(missing)}",
67
67
  f"missing: {_join_names(missing)}",
68
68
  )
69
69
  if assertion_type == "json_schema":
@@ -4,6 +4,7 @@ from collections import deque
4
4
  from dataclasses import dataclass
5
5
  import json
6
6
  import queue
7
+ import shutil
7
8
  import subprocess
8
9
  import threading
9
10
  import time
@@ -29,6 +30,29 @@ def _command_basename(program: str) -> str:
29
30
  return program.replace("\\", "/").rsplit("/", 1)[-1].lower()
30
31
 
31
32
 
33
+ def _prefer_available_python(command: Sequence[str]) -> list[str]:
34
+ """Run ``python3`` when a suite asks for bare ``python`` and only python3 exists.
35
+
36
+ Ubuntu, macOS, and pipx installs often have no ``python`` executable. Suites
37
+ generated before that was the default, and the checked-in examples, still say
38
+ ``python``. Use ``python3`` instead of failing those runs. An explicit path
39
+ such as ``/usr/bin/python`` is left unchanged.
40
+ """
41
+ if not command:
42
+ return list(command)
43
+ program = command[0]
44
+ normalized = program.replace("\\", "/")
45
+ if "/" in normalized:
46
+ return list(command)
47
+ if _command_basename(program) not in {"python", "python.exe"}:
48
+ return list(command)
49
+ if shutil.which(program):
50
+ return list(command)
51
+ if shutil.which("python3"):
52
+ return ["python3", *list(command[1:])]
53
+ return list(command)
54
+
55
+
32
56
  def _agent_start_error(command: Sequence[str], exc: BaseException) -> AgentProcessError:
33
57
  """Turn a failed exec into an actionable error for suite.yaml authors."""
34
58
  program = command[0] if command else ""
@@ -100,6 +124,7 @@ class AgentProcess:
100
124
  return
101
125
  if not self._command or not self._command[0].strip():
102
126
  raise _agent_start_error(self._command, ValueError("empty command"))
127
+ self._command = _prefer_available_python(self._command)
103
128
  try:
104
129
  self._process = subprocess.Popen(
105
130
  self._command,
@@ -21,11 +21,11 @@ _SENSITIVE_PARTS = {
21
21
 
22
22
  _PATTERNS: list[tuple[re.Pattern[str], str]] = [
23
23
  (re.compile(r"(?i)bearer\s+[a-z0-9._~+/=-]+"), "Bearer [REDACTED]"),
24
- (re.compile(r"\\bsk-[A-Za-z0-9]{20,}\\b"), "[REDACTED]"),
25
- (re.compile(r"\\bghp_[A-Za-z0-9]{36}\\b"), "[REDACTED]"),
26
- (re.compile(r"\\bAKIA[0-9A-Z]{16}\\b"), "[REDACTED]"),
24
+ (re.compile(r"\bsk-[A-Za-z0-9]{20,}\b"), "[REDACTED]"),
25
+ (re.compile(r"\bghp_[A-Za-z0-9]{36}\b"), "[REDACTED]"),
26
+ (re.compile(r"\bAKIA[0-9A-Z]{16}\b"), "[REDACTED]"),
27
27
  (
28
- re.compile(r"\\beyJ[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\b"),
28
+ re.compile(r"\beyJ[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9_-]{8,}\b"),
29
29
  "[REDACTED]",
30
30
  ),
31
31
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runledger
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: CI for tool-using agents: deterministic eval suites with record/replay, hard assertions, budgets, and PR regression gates.
5
5
  Author: RunLedger
6
6
  License-Expression: MIT
@@ -69,13 +69,13 @@ The same check as a GitHub Action. `suite.yaml` already points at `baselines/dem
69
69
 
70
70
  ```yaml
71
71
  - id: evals
72
- uses: runledger/Runledger@v0.2
72
+ uses: runledger/Runledger@v0.2.3
73
73
  with:
74
74
  path: ./evals/demo
75
75
  mode: replay
76
76
  ```
77
77
 
78
- This repo's `action.yml` also accepts a `baseline` input and sets `result` (`pass` or `fail`), `exit-code`, and `artifacts-path`. Those fields ship when a release containing this file is tagged. The in-repo self-test calls the action with `uses: ./`.
78
+ `runledger/Runledger@v0.2.3` accepts a `baseline` input and sets `result` (`pass` or `fail`), `exit-code`, and `artifacts-path`. The in-repo self-test calls the action with `uses: ./`.
79
79
 
80
80
  Page copy for runledger.io lives in `docs/site-quickstart.md`.
81
81
 
@@ -229,7 +229,7 @@ Optional:
229
229
 
230
230
  ```yaml
231
231
  suite_name: support-triage
232
- agent_command: ["python", "agent.py"]
232
+ agent_command: ["python3", "agent.py"]
233
233
  mode: replay # replay | record | live
234
234
  cases_path: cases
235
235
  tool_registry:
@@ -306,13 +306,10 @@ Each line is one tool invocation:
306
306
  MVP assertions:
307
307
 
308
308
  * `json_schema` (validate final output with JSON Schema)
309
- * `required_fields` (keys exist / basic typing)
310
- * `regex` / `contains` (for specific fields)
311
- * `tool_contract`:
312
-
313
- * must call tool X
314
- * must not call tool Y
315
- * X before Y (ordering)
309
+ * `required_fields` (keys exist)
310
+ * `must_call` (must call tool X)
311
+ * `must_not_call` (must not call tool Y)
312
+ * `call_order` (X before Y; tools do not have to be adjacent)
316
313
 
317
314
  Not default-gating (optional later):
318
315
 
@@ -323,18 +320,12 @@ Not default-gating (optional later):
323
320
 
324
321
  ## Budgets (merge gates)
325
322
 
326
- MVP budgets:
323
+ Budgets that fail a run:
327
324
 
328
325
  * `max_wall_ms`
329
326
  * `max_tool_calls`
330
327
  * `max_tool_errors`
331
328
 
332
- Not yet enforced (accepted in config, reported in artifacts when the agent sends metrics, but
333
- they do not fail a run today):
334
-
335
- * `max_tokens_out`
336
- * `max_cost_usd`
337
-
338
329
  ---
339
330
 
340
331
  ## Baselines + regression checks
@@ -389,7 +380,7 @@ jobs:
389
380
 
390
381
  - name: Run deterministic evals (replay)
391
382
  id: evals
392
- uses: runledger/Runledger@v0.2
383
+ uses: runledger/Runledger@v0.2.3
393
384
  with:
394
385
  path: ./evals/demo
395
386
  mode: replay
@@ -398,7 +389,7 @@ jobs:
398
389
  run: echo "result=${{ steps.evals.outputs.result }} exit=${{ steps.evals.outputs.exit-code }}"
399
390
  ```
400
391
 
401
- `runledger init` writes `.github/workflows/runledger.yml`, which installs the CLI and runs the suite with `--baseline`. The action above is the Marketplace entry in `action.yml`. It uploads `runledger_out/**` itself. On a release that contains this `action.yml`, the step also sets `result`, `exit-code`, and `artifacts-path`, and you can pass `baseline:` to override `suite.yaml`.
392
+ `runledger init` writes `.github/workflows/runledger.yml`, which installs the CLI and runs the suite with `--baseline`. The action above is the Marketplace entry in `action.yml` at `runledger/Runledger@v0.2.3`. It uploads `runledger_out/**` on success and on failure. The step sets `result`, `exit-code`, and `artifacts-path`, and you can pass `baseline:` to override `suite.yaml`. A failing run fails the job.
402
393
 
403
394
  ---
404
395
 
File without changes
File without changes