lisa-harness-e2e 0.1.0__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.
@@ -0,0 +1,33 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+
9
+
10
+ # Virtual environments
11
+ .venv/
12
+
13
+ # IDE
14
+ .vscode/
15
+
16
+ # OS
17
+ .DS_Store
18
+ Thumbs.db
19
+
20
+ # Testing
21
+ .import_linter_cache/
22
+ .pytest_cache/
23
+ .ruff_cache/
24
+ .mypy_cache/
25
+ .lisa-tmp/
26
+ coverage/
27
+ .coverage
28
+ .coverage.*
29
+ htmlcov/
30
+
31
+ # Outputs
32
+ artifacts/
33
+ .e2e/
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.5
2
+ Name: lisa-harness-e2e
3
+ Version: 0.1.0
4
+ Summary: End-to-end scenario harness: drives a full Lisa runtime through pack workflows
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: lisa-harness-kernel
7
+ Requires-Dist: lisa-harness-runtime
8
+ Requires-Dist: lisa-harness-sdk
9
+ Requires-Dist: python-dotenv>=1.0
10
+ Requires-Dist: pyyaml>=6.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
13
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
@@ -0,0 +1,269 @@
1
+ # e2etest — live end-to-end pack scenarios (`lisa-e2e`)
2
+
3
+ `e2etest` drives a **real** `HostRuntime` with a **live model** through a
4
+ scripted pack workflow, then judges the outcome from on-disk evidence. It is
5
+ the layer above [`packstest`](../packstest/README.md):
6
+
7
+ | Layer | Drives | Model | Answers |
8
+ | --- | --- | --- | --- |
9
+ | `packstest` evals | one tool in-process | none | "Does the tool honor its contract?" |
10
+ | `lisa-e2e` scenarios | the whole runtime + `handle_message` | live | "Can Lisa actually complete the workflow?" |
11
+
12
+ Use `packstest` for fast, deterministic regressions. Use `lisa-e2e` when the
13
+ question is whether routing, handoffs, approvals, policy, memory, sandboxing,
14
+ and the pack's tools work together for a real model.
15
+
16
+ See the [documentation index](../docs/README.md) and [testing](../docs/TESTING.md).
17
+ The workspace installs `lisa-e2e`; the framework's current publish workflow
18
+ does not publish the `lisa-harness-e2e` distribution.
19
+
20
+ ## How a run works
21
+
22
+ ```mermaid
23
+ flowchart LR
24
+ S[scenario.yml] --> V[packs venv<br/>editable installs]
25
+ S --> R[run dir<br/>isolated data dir + fixture repo]
26
+ V --> D[driver: build_runtime + handle_message]
27
+ R --> D
28
+ D -->|turns, /approve, /deny| D
29
+ D --> O[oracles over evidence]
30
+ O --> B[verdict.json + transcript.md]
31
+ ```
32
+
33
+ 1. **Packs venv** — `.e2e/cache/packs-venv-<hash>` holds editable installs of
34
+ kernel/sdk/mcp_server plus the scenario's `packs`, sourced from
35
+ [`packs.yml`](packs.yml). Editable means a pack fix is live on the next run.
36
+ It is rebuilt only when the pack set or source paths change.
37
+ 2. **Run dir** — `.e2e/runs/<scenario-id>/<UTC stamp>/` (gitignored). Contains
38
+ a fresh `data/` dir used as `LISA_DATA_DIR`, the fixture repo copied to
39
+ `workspace/<name>` with a git history and a bare `remote.git`, and any
40
+ `fixture.data_files` tree copied into the storage sandbox (`data/data/files`).
41
+ Only `config/models.yaml` and `.env` are read from the base data dir
42
+ (`~/.lisa`, override with `LISA_E2E_BASE_DATA_DIR`).
43
+ 3. **Driver** — boots the runtime in-process, sends each scripted turn, then
44
+ follows up until the turn finishes:
45
+ - approval pending → `/approve <id>` or `/deny <id>` per the scenario rules
46
+ (the real approval path, not a policy bypass);
47
+ - yield → `yield_reply`, only if `max_yield_replies` allows;
48
+ - pause/stop exit → `continue_message`, only if `max_continues` allows.
49
+ 4. **Oracles** — assertions over the thread log, memory DB, code-change
50
+ artifacts, runtime log, and git. They never trust the model's prose.
51
+ 5. **Bundle** — `verdict.json` (first failure first), `transcript.md`
52
+ (exchanges + per-turn receipts), `outcome.json`, and the whole `data/` dir.
53
+
54
+ ## Running the existing scenarios
55
+
56
+ From the framework checkout:
57
+
58
+ ```bash
59
+ uv sync --frozen --extra dev # once
60
+ uv run --frozen lisa-e2e run sdk/packs/recovery/tests/e2e/skip_all.scenario.yml
61
+ ```
62
+
63
+ This bundled scenario needs the configured base roster/provider **and**
64
+ external bash/bwrap/markdown/personas sources named in its `packs` list.
65
+ Inspect the scenario and `packs.yml` before running; a scenario located
66
+ in this checkout is not necessarily self-contained.
67
+
68
+ | Scenario | Workflow |
69
+ | --- | --- |
70
+ | `coding/review-fix-commit` | knowledge-capturing review → worktree → patch → required check → commit, verified by re-running the tests outside Lisa |
71
+ | `coding/feature-design-implement` | product review → feature design doc → multi-file implementation in a worktree → required check → commit, judged by hidden acceptance tests |
72
+ | `coding/roadmap-chunk-implement` | open-ended product review → roadmap → model-proposed design overridden by operator pushback → terse "start the first chunk" that must recover the agreed design → checked commit, same hidden acceptance tests |
73
+ | `finances/categorize-and-close` | import bank + card exports → survey + proposed categories → user decisions (incl. a new category) → final ledger, verified by re-reading the ledger outside Lisa |
74
+
75
+ Options:
76
+
77
+ - `--rebuild-venv` — force a packs-venv rebuild (after editing `packs.yml` or a
78
+ pack's dependencies; source edits do not need it).
79
+ - `--runs-dir DIR` — write bundles elsewhere.
80
+
81
+ Exit codes: `0` pass, `1` an oracle failed, `2` setup error (bad scenario,
82
+ missing base `models.yaml`, venv build failure).
83
+
84
+ A run takes minutes and costs real tokens. Live runs vary between attempts, so
85
+ confirm a fix with more than one passing run before calling it done.
86
+
87
+ ## Debugging a failed run
88
+
89
+ 1. Read `verdict.json` → `first_failure` (oracle, args, detail).
90
+ 2. Read `transcript.md` for the exchange sequence, approvals, and the
91
+ per-turn receipt list.
92
+ 3. Go to the raw evidence in `data/`:
93
+ - `logs/runtime.jsonl` — warnings/errors (operation timeouts, validation
94
+ failures, truncation retries);
95
+ - `data/threads/*.current.jsonl` — every turn's transcript, tool calls with
96
+ exact arguments, `turn_input` requests, provenance;
97
+ - `state/code-change-artifacts.json` — WorkProduct state, check records;
98
+ - `data/memory.db` — what memory actually stored.
99
+ 4. Reproduce the failing piece **deterministically** before fixing: replay the
100
+ exact tool arguments from the thread against `git`/the tool, or boot the
101
+ runtime and call the pool directly (no model, seconds):
102
+
103
+ ```python
104
+ rt = build_runtime(host_kind="e2e", data_dir=..., session_context=..., in_process_mcp=True)
105
+ await rt.start()
106
+ await rt.resources.mcp_tool_pool.call_tool("memory.add", {...})
107
+ ```
108
+
109
+ 5. Fix the root cause (pack, kernel, or runtime), add a regression test at the
110
+ lowest layer that reproduces it, then rerun the scenario.
111
+
112
+ Never loosen an oracle to get green. If the harness mis-modelled a contract,
113
+ fix the harness and add a test in [`tests/`](tests).
114
+
115
+ ## Writing a scenario
116
+
117
+ Scenarios live **in the pack**, next to its tests:
118
+
119
+ ```text
120
+ <pack>/tests/e2e/
121
+ conftest.py # collect_ignore_glob = ["fixtures/*"]
122
+ <name>.scenario.yml
123
+ session_context.md # template; ${...} placeholders are substituted
124
+ fixtures/<repo>/ # optional seed repo (planted defect, sample data)
125
+ ```
126
+
127
+ `conftest.py` keeps pytest from collecting fixture code (which is deliberately
128
+ broken). Give fixture repos a realistic `.gitignore`.
129
+
130
+ ### Scenario file reference
131
+
132
+ ```yaml
133
+ id: <pack>/<workflow> # bundle dir name; defaults to the file stem
134
+ session_context: session_context.md
135
+ fixture:
136
+ repo: fixtures/<repo> # optional; becomes a git repo with a bare origin
137
+ data_files: fixtures/<tree> # optional; copied into <data_dir>/data/files
138
+ packs: [lisa-pack-...] # keys of e2etest/packs.yml installed into the venv
139
+ model:
140
+ script: model/<name>.py # optional; scripted model instead of the live LLM
141
+ policy: policy.yaml # optional; default is empty `mode: ask`
142
+ env: {NAME: value} # optional extra process env
143
+ turns: # user messages, in order
144
+ - "..."
145
+ approvals:
146
+ approve: [tool_id, ...] # "*" approves everything not denied
147
+ deny: [tool_id, ...] # unlisted targets are denied
148
+ max_decisions: 20
149
+ budgets:
150
+ max_messages: 16 # every message sent, including /approve
151
+ max_continues: 0 # replies to pause exits
152
+ max_yield_replies: 0 # replies to model questions
153
+ wall_clock_s: 1800
154
+ continue_message: continue
155
+ yield_reply: Proceed with your best judgment.
156
+ oracles:
157
+ <phase>: # free-form grouping shown in the verdict
158
+ - <oracle_name>: {args}
159
+ ```
160
+
161
+ `session_context.md` placeholders: `${WORKSPACE}` (fixture repo path),
162
+ `${WORKSPACE_PARENT}` (where coding worktrees go), `${RUN_DIR}`. Mirror the
163
+ real session the pack targets, but name it uniquely (`name: e2e-<pack>`) so
164
+ live policy rules keyed on a workspace id do not apply.
165
+
166
+ ### Scripted model (token-free repros)
167
+
168
+ `model.script` replaces every provider gateway with a Python module, so a
169
+ scenario replays a fixed tool sequence in seconds at zero token cost.
170
+ Everything below the model (routing, policy, approvals, packs, sandbox,
171
+ persistence) is production code. Use it to pin down a bug a live run
172
+ surfaced, not to judge model behavior.
173
+
174
+ The module defines `respond(turn: ScriptTurn) -> Reply` and, optionally,
175
+ `auxiliary(turn)` for tool-less requests (distillers, classifiers, titles).
176
+ Those otherwise receive `{}` or empty text, which some operations log as
177
+ errors. `ScriptTurn` exposes `tool_names`, `user_text`, `count(name)`, and
178
+ `last(name)`. `last(name).data` is the JSON object inside that tool's
179
+ result, so ids and digests flow from real tool output. Return
180
+ `Reply.call(tool, **args)`, `Reply.calls(...)`, or `Reply.text(...)`; raise
181
+ `ScriptError` when the transcript is not what the script expects. Add
182
+ `"model/*"` to the pack's `conftest.py` `collect_ignore_glob`. See
183
+ `coding/tests/e2e/edit_after_check.scenario.yml` in the coding pack.
184
+
185
+ ### Oracle reference
186
+
187
+ Conversation hygiene:
188
+
189
+ | Oracle | Args | Passes when |
190
+ | --- | --- | --- |
191
+ | `not_aborted` | — | no budget was exceeded |
192
+ | `no_turn_errors` | — | no exchange errored |
193
+ | `no_operator_exit` | — | no pause/stop/terminate exit |
194
+ | `no_unanswered_yield` | — | the conversation did not end on a question |
195
+ | `no_runtime_errors` | `ignore: [regex]` | no error-level record in `runtime.jsonl` |
196
+ | `max_exchanges` | `n` | at most `n` messages were sent |
197
+
198
+ Capability and knowledge:
199
+
200
+ | Oracle | Args | Passes when |
201
+ | --- | --- | --- |
202
+ | `receipt` | `capability` (id or list), `min` | ≥ `min` effect receipts for those capabilities |
203
+ | `memory_recorded` | `mentions_any`, `min` | ≥ `min` stored memories mention any term |
204
+
205
+ Code change (coding pack WorkProduct + git; needs a `fixture.repo`):
206
+
207
+ | Oracle | Args | Passes when |
208
+ | --- | --- | --- |
209
+ | `workproduct_committed` | — | an artifact has a `commit_sha` |
210
+ | `branch_prefix` | `prefix` | the committed branch starts with `prefix` |
211
+ | `main_untouched` | — | local and remote `main` equal the fixture's initial commit |
212
+ | `diff_touches` | `paths`, `exact`, `allow` | the commit changes `paths` (and nothing outside `paths` + `allow` when `exact`) |
213
+ | `checks_current` | — | every required check passed at the artifact revision and committed tree |
214
+ | `worktree_clean` | — | the owned worktree is clean and at the commit |
215
+ | `baseline_fails` | `argv`, `timeout_s` | `argv` fails on the fixture baseline (the defect is real) |
216
+ | `independent_check` | `argv`, `timeout_s`, `restore` | `argv` passes on a fresh checkout of the commit, outside Lisa, with `restore` paths reset to the fixture baseline |
217
+
218
+ `baseline_fails` and `independent_check` run with cwd at the checkout and
219
+ expand `${SCENARIO_DIR}` and `${RUN_DIR}` in `argv`, so hidden acceptance
220
+ tests can live beside the scenario where the model cannot see or edit them.
221
+
222
+ Storage sandbox (packs whose work product lives under `data/files`):
223
+
224
+ | Oracle | Args | Passes when |
225
+ | --- | --- | --- |
226
+ | `data_check` | `argv`, `timeout_s` | `argv` exits 0 with cwd at the storage root; `${SCENARIO_DIR}`, `${FILES}`, `${RUN_DIR}` expand |
227
+
228
+ An unknown oracle name fails the run, so typos surface immediately.
229
+
230
+ ### Design rules for good scenarios
231
+
232
+ - **Assert outcomes, not prose.** Prefer receipts, stored records, git state,
233
+ and an independent check over text matching.
234
+ - **Prove the fixture.** Pair every "fixed" oracle with a baseline oracle that
235
+ shows the starting state was actually broken.
236
+ - **Allow tests, judge with the originals.** A fix may add regression tests:
237
+ list test files under `diff_touches.allow` and pair it with an
238
+ `independent_check` that `restore`s them, so edited tests cannot mask an
239
+ unfixed defect.
240
+ - **Verify outside Lisa.** End-state checks (`independent_check`, a re-read of
241
+ the ledger/file) must not reuse the tool path under test.
242
+ - **Exercise approvals for real.** Approve the effectful tools the workflow
243
+ needs; deny the ones it must not reach (e.g. `coding_push`).
244
+ - **Default to strict budgets.** `max_continues: 0` and `max_yield_replies: 0`
245
+ make stalls and capability gaps fail loudly instead of being papered over.
246
+ - **Keep fixtures tiny and deterministic.** Stdlib-only test commands, no
247
+ network, one planted defect per workflow.
248
+ - **Judge features with hidden tests.** For feature work, keep the acceptance
249
+ suite outside the fixture (`${SCENARIO_DIR}/acceptance/`) and pair a
250
+ `baseline_fails` with an `independent_check` on it.
251
+
252
+ ### Adding a new pack to the harness
253
+
254
+ 1. Add the pack's source path to [`packs.yml`](packs.yml) (relative to that
255
+ file). The `packs` list in a scenario may only name keys from it.
256
+ 2. If the scenario needs an assertion the oracle table lacks, add it to
257
+ [`oracles.py`](src/e2etest/oracles.py) with the `@oracle("name")`
258
+ decorator and a test in [`tests/test_harness.py`](tests/test_harness.py)
259
+ that covers both pass and fail.
260
+
261
+ ## Gotchas
262
+
263
+ - The coding pack only runs checks under a bwrap floor with
264
+ `execution: sandbox` and `persistence: direct-write`, and the worktree parent
265
+ (`${WORKSPACE_PARENT}`) must be bound `rw`.
266
+ - The base `.env` is loaded without overriding the process env; location
267
+ overrides (`LISA_PROFILE`, `LISA_THREAD_DIR`, `LISA_SESSION_CONTEXT_PATH`,
268
+ `LISA_MCP_LOG_PATH`) are cleared so a run can never touch live state.
269
+ - Run one scenario per process; the runtime keeps process-global registries.
@@ -0,0 +1,17 @@
1
+ # Pack sources installed (editable) into the e2e packs venv.
2
+ # Paths are relative to this file. Editable installs mean a pack fix is live on
3
+ # the next scenario run without a reinstall.
4
+ framework:
5
+ - ../kernel
6
+ - ../sdk
7
+ - ../mcp_server
8
+ packs:
9
+ lisa-pack-bash: ../../lisa-packs/bash
10
+ lisa-pack-bwrap: ../../lisa-packs/bwrap
11
+ lisa-pack-coding: ../../packs-wip/coding
12
+ lisa-pack-finances: ../../packs-wip/finances
13
+ lisa-pack-markdown: ../../lisa-packs/markdown
14
+ lisa-pack-personas: ../../lisa-packs/personas
15
+ lisa-pack-recovery: ../sdk/packs/recovery
16
+ lisa-pack-search: ../../lisa-packs/search
17
+ lisa-pack-serper: ../../lisa-packs/serper
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.build.targets.wheel]
6
+ packages = ["src/e2etest"]
7
+
8
+ [project]
9
+ name = "lisa-harness-e2e"
10
+ version = "0.1.0"
11
+ description = "End-to-end scenario harness: drives a full Lisa runtime through pack workflows"
12
+ requires-python = ">=3.11"
13
+
14
+ dependencies = [
15
+ "lisa-harness-kernel",
16
+ "lisa-harness-sdk",
17
+ "lisa-harness-runtime",
18
+ "python-dotenv>=1.0",
19
+ "pyyaml>=6.0",
20
+ ]
21
+
22
+ [project.scripts]
23
+ lisa-e2e = "e2etest.cli:main"
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "pytest>=8.0.0",
28
+ "pytest-asyncio>=0.23.0",
29
+ ]
30
+
31
+ [tool.pytest.ini_options]
32
+ pythonpath = ["src"]
33
+ testpaths = ["tests"]
34
+
35
+ [tool.uv]
36
+ package = true
37
+
38
+ [tool.uv.sources]
39
+ lisa-harness-kernel = { workspace = true }
40
+ lisa-harness-sdk = { workspace = true }
41
+ lisa-harness-runtime = { workspace = true }
@@ -0,0 +1 @@
1
+ """End-to-end scenario harness for Lisa packs (drives a full ``HostRuntime``)."""
@@ -0,0 +1,67 @@
1
+ """Write a run bundle: verdict, raw outcome, and a readable transcript."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from e2etest.oracles import Evidence, OracleResult
11
+ from e2etest.scenario import Scenario
12
+
13
+ _TEXT_LIMIT = 4000
14
+
15
+
16
+ def verdict(scenario: Scenario, evidence: Evidence, results: list[OracleResult]) -> dict[str, Any]:
17
+ failed = [r for r in results if not r.passed]
18
+ return {
19
+ "scenario": scenario.scenario_id,
20
+ "scenario_file": str(scenario.source),
21
+ "run_dir": str(evidence.paths.run_dir),
22
+ "passed": not failed,
23
+ "first_failure": asdict(failed[0]) if failed else None,
24
+ "exchanges": len(evidence.outcome.exchanges),
25
+ "aborted": evidence.outcome.aborted,
26
+ "results": [asdict(r) for r in results],
27
+ }
28
+
29
+
30
+ def _transcript(evidence: Evidence) -> str:
31
+ lines = ["# Transcript", ""]
32
+ for ex in evidence.outcome.exchanges:
33
+ lines.append(f"## #{ex.index} {ex.kind} ({ex.duration_s}s)")
34
+ lines += ["", "```text", ex.message, "```", ""]
35
+ flags = []
36
+ if ex.approval:
37
+ flags.append(
38
+ f"approval: {ex.approval.get('target_id')} ({ex.approval.get('request_id')})"
39
+ )
40
+ if ex.operator_exit:
41
+ flags.append(f"operator_exit: {ex.operator_exit}")
42
+ if ex.yielded_question:
43
+ flags.append(f"yielded: {ex.yielded_question}")
44
+ flags += [f"error: {e}" for e in ex.errors]
45
+ lines += [f"- {f}" for f in flags]
46
+ text = ex.text if len(ex.text) <= _TEXT_LIMIT else ex.text[:_TEXT_LIMIT] + "\n…[truncated]"
47
+ lines += ["", text, ""]
48
+ lines += ["# Thread entries", ""]
49
+ for entry in evidence.thread_entries:
50
+ receipts = [r.get("capability_id") for r in entry.get("effect_receipts") or ()]
51
+ provenance = entry.get("provenance") or {}
52
+ lines.append(
53
+ f"- turn {entry.get('turn')} mode={entry.get('mode_id')} "
54
+ f"handoff={entry.get('handoff_to') or '-'} model={provenance.get('model_id', '-')} "
55
+ f"exit={provenance.get('operator_exit') or '-'} receipts={receipts}"
56
+ )
57
+ return "\n".join(lines) + "\n"
58
+
59
+
60
+ def write_bundle(run_dir: Path, payload: dict[str, Any], evidence: Evidence) -> Path:
61
+ (run_dir / "outcome.json").write_text(
62
+ json.dumps(evidence.outcome.to_json(), indent=1), encoding="utf-8"
63
+ )
64
+ (run_dir / "transcript.md").write_text(_transcript(evidence), encoding="utf-8")
65
+ path = run_dir / "verdict.json"
66
+ path.write_text(json.dumps(payload, indent=1), encoding="utf-8")
67
+ return path
@@ -0,0 +1,63 @@
1
+ """``lisa-e2e``: run one scenario end to end and write a verdict bundle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from datetime import UTC, datetime
8
+ from pathlib import Path
9
+
10
+ from e2etest.bundle import verdict, write_bundle
11
+ from e2etest.driver import run_conversation
12
+ from e2etest.environment import prepare_run
13
+ from e2etest.oracles import Evidence, evaluate
14
+ from e2etest.packs_venv import ensure_packs_venv
15
+ from e2etest.scenario import load_scenario
16
+
17
+ _REPO_ROOT = Path(__file__).resolve().parents[3]
18
+ _EXIT_FAIL = 1
19
+ _EXIT_SETUP = 2
20
+
21
+
22
+ def _parser() -> argparse.ArgumentParser:
23
+ parser = argparse.ArgumentParser(prog="lisa-e2e", description=__doc__)
24
+ sub = parser.add_subparsers(dest="command", required=True)
25
+ run = sub.add_parser("run", help="Run a scenario against a live model.")
26
+ run.add_argument("scenario", type=Path)
27
+ run.add_argument("--runs-dir", type=Path, default=_REPO_ROOT / ".e2e" / "runs")
28
+ run.add_argument("--rebuild-venv", action="store_true")
29
+ return parser
30
+
31
+
32
+ def _run(args: argparse.Namespace) -> int:
33
+ scenario = load_scenario(args.scenario)
34
+ venv = ensure_packs_venv(
35
+ scenario.packs, _REPO_ROOT / ".e2e" / "cache", rebuild=args.rebuild_venv
36
+ )
37
+ stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
38
+ run_dir = (args.runs_dir / scenario.scenario_id.replace("/", "__") / stamp).resolve()
39
+ paths = prepare_run(scenario, run_dir, venv)
40
+ print(f"run dir: {run_dir}", flush=True)
41
+ outcome = run_conversation(scenario, paths, venv)
42
+ evidence = Evidence(paths, outcome)
43
+ payload = verdict(scenario, evidence, evaluate(scenario.oracles, evidence))
44
+ path = write_bundle(run_dir, payload, evidence)
45
+ for result in payload["results"]:
46
+ mark = "PASS" if result["passed"] else "FAIL"
47
+ first = result["detail"].splitlines()[0] if result["detail"] else ""
48
+ print(f"{mark} {result['phase']}.{result['name']}: {first}")
49
+ print(f"verdict: {'PASS' if payload['passed'] else 'FAIL'} → {path}")
50
+ return 0 if payload["passed"] else _EXIT_FAIL
51
+
52
+
53
+ def main(argv: list[str] | None = None) -> int:
54
+ args = _parser().parse_args(argv)
55
+ try:
56
+ return _run(args)
57
+ except (OSError, ValueError, RuntimeError) as exc:
58
+ print(f"setup error: {type(exc).__name__}: {exc}", file=sys.stderr)
59
+ return _EXIT_SETUP
60
+
61
+
62
+ if __name__ == "__main__":
63
+ sys.exit(main())