diff --git a/README.md b/README.md index 92873ac..fc7361e 100644 --- a/README.md +++ b/README.md @@ -189,8 +189,11 @@ gate PASSED — receipt chain validates (9 receipts). Mock mode swaps only the *model content* for a bundled sample task — pytest, ultracode, the panel, and the gate all really execute, which is what makes -the no-key demo honest. Commands: `bob run ` · `bob gate` -(pre-commit chokepoint; `--complete-on-pass` seals the run) · `bob status`. +the no-key demo honest. bob's one runtime dependency is pytest +(`pip install pytest`, or `agent-ultra-kit[bob]`); the rest of the kit needs +nothing. Commands: `bob run ` · `bob gate --mark-pass` (pre-commit: +validates, keeps the run active) · `bob seal` (post-commit: releases it) · +`bob hook-install` (writes all three hooks) · `bob status`. Full guide: [docs/bob-the-builder.md](docs/bob-the-builder.md). ## What's in the box diff --git a/docs/bob-the-builder.md b/docs/bob-the-builder.md index 18bf8f4..c05ad50 100644 --- a/docs/bob-the-builder.md +++ b/docs/bob-the-builder.md @@ -35,6 +35,7 @@ cross-check (ultracode receipts, the panel execution receipt). ## Quick start (no API key) ```bash +pip install pytest # bob's only runtime dependency (RED/GREEN run it) agent-ultra bob run "add a slugify helper" --mock ``` diff --git a/pyproject.toml b/pyproject.toml index 86f51fb..e956c9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,10 @@ dependencies = [] [project.optional-dependencies] dev = ["pytest>=7"] +# bob's RED/GREEN steps execute pytest at runtime; the core kit stays +# dependency-free, so install this extra (or pytest itself) to use bob: +# pip install agent-ultra-kit[bob] +bob = ["pytest>=7"] # Optional heavy worker: LangChain Deep Agents as a multi-file builder/fixer. # The core install stays zero-dependency; install this extra only if you want # the Deep Agents worker: pip install agent-ultra-kit[deepagents] diff --git a/src/agent_ultra/adapters/doctor.py b/src/agent_ultra/adapters/doctor.py index 74508fd..e6d10c6 100644 --- a/src/agent_ultra/adapters/doctor.py +++ b/src/agent_ultra/adapters/doctor.py @@ -256,10 +256,17 @@ def run_check(cmd, cwd): def bob_demo(out_dir: Path): """Offline: the full 10-step pipeline passes its gate, then three faked chains block — a skipped step, a fabricated fan-out claim, and a doctored - panel receipt. All state is isolated under *out_dir*.""" + panel receipt. All state is isolated under *out_dir*. + + Returns (ok, info); ok is None when pytest is absent (the kit core is + dependency-free, so a fresh install may not have it) — callers report + that as a WARN with the install hint, not a failure.""" import io from ..bob import run_bob, gate_check - from ..bob.pipeline import BobRun + from ..bob.pipeline import BobRun, pytest_available, PYTEST_MISSING_MSG + + if not pytest_available(): + return None, {"skipped": PYTEST_MISSING_MSG} ws = out_dir / "bob-demo" key = out_dir / "bob-demo.key" @@ -267,6 +274,10 @@ def bob_demo(out_dir: Path): sink = io.StringIO() outcome = run_bob("add a slugify helper", ws, mock=True, key_path=key, ultracode_home=uc_home, interactive=False, out=sink) + if not outcome.passed: + return False, {"pipeline_passed": False, + "blocked_at": outcome.blocked_at, + "run_id": outcome.run_id} run = BobRun(workspace=ws, run_dir=ws / ".agent-ultra" / "bob" / "runs" / outcome.run_id, key=key.read_bytes()) @@ -336,11 +347,17 @@ def run_demo(args) -> int: f"-> {uc['final_state']} -> receipt valid={uc['receipt_valid']} " f"-> resume replays {uc['resume_cached']} cached ({uc['resume_calls']} calls)") ok7, bb = bob_demo(out) - print(f"[{'ok' if ok7 else 'FAIL'}] bob: 10-step pipeline gate " - f"passed={bb['pipeline_passed']}; skipped step blocks=" - f"{bb['skipped_step_blocks']}, fake fan-out blocks=" - f"{bb['fake_fanout_blocks']}, doctored panel blocks=" - f"{bb['fake_panel_blocks']}") + if ok7 is None: + print(f"[--] bob: skipped — {bb['skipped']}") + ok7 = True # absence of the optional test runner is not a failure + elif ok7: + print(f"[ok] bob: 10-step pipeline gate " + f"passed={bb['pipeline_passed']}; skipped step blocks=" + f"{bb['skipped_step_blocks']}, fake fan-out blocks=" + f"{bb['fake_fanout_blocks']}, doctored panel blocks=" + f"{bb['fake_panel_blocks']}") + else: + print(f"[FAIL] bob: {json.dumps(bb)[:160]}") print(f"\nartifacts: {out}") all_ok = ok1 and ok2 and ok3 and ok4 and ok5 and ok6 and ok7 print("\nDEMO " + ("PASSED — the full loop works on this machine." @@ -445,12 +462,18 @@ def add(status, name, detail=""): except Exception as e: add(FAIL, "ultracode", str(e)[:160]) - # 7a3. bob: the 10-step pipeline passes its gate; faked chains block + # 7a3. bob: the 10-step pipeline passes its gate; faked chains block. + # pytest is bob's only runtime dependency and is optional for the rest + # of the kit — absent pytest is a WARN with the install hint, not a FAIL. try: ok, detail = bob_demo(scratch) - add(PASS if ok else FAIL, - "bob: 10-step pipeline gate passes; skipped/faked steps block", - json.dumps(detail)[:140]) + if ok is None: + add(WARN, "bob: skipped — pytest not installed", + "RED/GREEN need it: pip install pytest") + else: + add(PASS if ok else FAIL, + "bob: 10-step pipeline gate passes; skipped/faked steps block", + json.dumps(detail)[:140]) except Exception as e: add(FAIL, "bob pipeline", str(e)[:160]) diff --git a/src/agent_ultra/bob/pipeline.py b/src/agent_ultra/bob/pipeline.py index 19a8531..73d3bba 100644 --- a/src/agent_ultra/bob/pipeline.py +++ b/src/agent_ultra/bob/pipeline.py @@ -283,6 +283,19 @@ def complete(self, gate_result: dict) -> None: # system pytest runner (steps 2 and 3) # -------------------------------------------------------------------------- +def pytest_available() -> bool: + """The RED/GREEN steps execute pytest; the kit core is dependency-free, + so pytest may be absent on a fresh install. Callers check this to give a + clear 'pip install pytest' message instead of a cryptic empty RED.""" + import importlib.util + return importlib.util.find_spec("pytest") is not None + + +PYTEST_MISSING_MSG = ("pytest is not installed in this environment — the " + "RED/GREEN steps need it: pip install pytest " + "(or: pip install 'agent-ultra-kit[dev]')") + + # -v node lines: "tests/test_x.py::test_a PASSED [ 50%]" and parametrized ids # that contain spaces: "test_x.py::test_a[hello world] PASSED [ 50%]". The id # is everything up to the status word, which pytest right-pads before the @@ -321,6 +334,10 @@ def run_pytest_step(run: BobRun, step: str, test_file: str, if step not in ("red", "green"): raise ValueError("step must be 'red' or 'green'") step_name = "step02_red" if step == "red" else "step03_green" + if not pytest_available(): + # write nothing: an honest "cannot run" beats a receipt recording a + # spawn failure as if the tests were exercised + return False, {}, PYTEST_MISSING_MSG from ..broker.broker import classify shown_cmd = f"{Path(sys.executable).name} -m pytest {test_file} -v" @@ -609,7 +626,9 @@ def _counts(ev) -> dict: checked["step03_green"] = f"{len(green_ids)} tests passing" # ---- live re-run (re-derived, never trusted) --------------------------- - if rerun_tests and green is not None and test_file: + if rerun_tests and green is not None and test_file and not pytest_available(): + errors.append(f"live re-run: {PYTEST_MISSING_MSG}") + elif rerun_tests and green is not None and test_file: exit_code, output = _run_pytest(run.workspace, test_file, timeout=600) live_ids = set(parse_pytest_nodes(output)) if exit_code != 0: